/*!
 * jsoneditor.js
 *
 * @brief
 * JSONEditor is a web-based tool to view, edit, and format JSON.
 * It shows data a clear, editable treeview.
 *
 * Supported browsers: Chrome, Firefox, Safari, Opera, Internet Explorer 8+
 *
 * @license
 * This json editor is open sourced with the intention to use the editor as
 * a component in your own application. Not to just copy and monetize the editor
 * as it is.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy
 * of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 *
 * Copyright (c) 2011-2013 Jos de Jong, http://jsoneditoronline.org
 *
 * @author  Jos de Jong, <wjosdejong@gmail.com>
 * @version 2.3.3
 * @date    2013-10-17
 */
(function () {

/**
 * @constructor JSONEditor
 * @param {Element} container    Container element
 * @param {Object}  [options]    Object with options. available options:
 *                               {String} mode      Editor mode. Available values:
 *                                                  'tree' (default), 'view',
 *                                                  'form', 'text', and 'code'.
 *                               {function} change  Callback method, triggered
 *                                                  on change of contents
 *                               {Boolean} search   Enable search box.
 *                                                  True by default
 *                                                  Only applicable for modes
 *                                                  'tree', 'view', and 'form'
 *                               {Boolean} history  Enable history (undo/redo).
 *                                                  True by default
 *                                                  Only applicable for modes
 *                                                  'tree', 'view', and 'form'
 *                               {String} name      Field name for the root node.
 *                                                  Only applicable for modes
 *                                                  'tree', 'view', and 'form'
 *                               {Number} indentation   Number of indentation
 *                                                      spaces. 4 by default.
 *                                                      Only applicable for
 *                                                      modes 'text' and 'code'
 * @param {Object | undefined} json JSON object
 */
function JSONEditor (container, options, json) {
    if (!(this instanceof JSONEditor)) {
        throw new Error('JSONEditor constructor called without "new".');
    }

    if (arguments.length) {
        this._create(container, options, json);
    }
}

/**
 * Configuration for all registered modes. Example:
 * {
 *     tree: {
 *         editor: TreeEditor,
 *         data: 'json'
 *     },
 *     text: {
 *         editor: TextEditor,
 *         data: 'text'
 *     }
 * }
 *
 * @type { Object.<String, {editor: Object, data: String} > }
 */
JSONEditor.modes = {};

/**
 * Create the JSONEditor
 * @param {Element} container    Container element
 * @param {Object}  [options]    See description in constructor
 * @param {Object | undefined} json JSON object
 * @private
 */
JSONEditor.prototype._create = function (container, options, json) {
    this.container = container;
    this.options = options || {};
    this.json = json || {};

    var mode = this.options.mode || 'tree';
    this.setMode(mode);
};

/**
 * Detach the editor from the DOM
 * @private
 */
JSONEditor.prototype._delete = function () {};

/**
 * Set JSON object in editor
 * @param {Object | undefined} json      JSON data
 */
JSONEditor.prototype.set = function (json) {
    this.json = json;
};

/**
 * Get JSON from the editor
 * @returns {Object} json
 */
JSONEditor.prototype.get = function () {
    return this.json;
};

/**
 * Set string containing JSON for the editor
 * @param {String | undefined} jsonText
 */
JSONEditor.prototype.setText = function (jsonText) {
    this.json = util.parse(jsonText);
};

/**
 * Get stringified JSON contents from the editor
 * @returns {String} jsonText
 */
JSONEditor.prototype.getText = function () {
    return JSON.stringify(this.json);
};

/**
 * Set a field name for the root node.
 * @param {String | undefined} name
 */
JSONEditor.prototype.setName = function (name) {
    if (!this.options) {
        this.options = {};
    }
    this.options.name = name;
};

/**
 * Get the field name for the root node.
 * @return {String | undefined} name
 */
JSONEditor.prototype.getName = function () {
    return this.options && this.options.name;
};

/**
 * Change the mode of the editor.
 * JSONEditor will be extended with all methods needed for the chosen mode.
 * @param {String} mode     Available modes: 'tree' (default), 'view', 'form',
 *                          'text', and 'code'.
 */
JSONEditor.prototype.setMode = function (mode) {
    var container = this.container,
        options = util.extend({}, this.options),
        data,
        name;

    options.mode = mode;
    var config = JSONEditor.modes[mode];
    if (config) {
        try {
            if (config.data == 'text') {
                // text
                name = this.getName();
                data = this.getText();

                this._delete();
                util.clear(this);
                util.extend(this, config.editor.prototype);
                this._create(container, options);

                this.setName(name);
                this.setText(data);
            }
            else {
                // json
                name = this.getName();
                data = this.get();

                this._delete();
                util.clear(this);
                util.extend(this, config.editor.prototype);
                this._create(container, options);

                this.setName(name);
                this.set(data);
            }

            if (typeof config.load === 'function') {
                try {
                    config.load.call(this);
                }
                catch (err) {}
            }
        }
        catch (err) {
            this._onError(err);
        }
    }
    else {
        throw new Error('Unknown mode "' + options.mode + '"');
    }
};

/**
 * Throw an error. If an error callback is configured in options.error, this
 * callback will be invoked. Else, a regular error is thrown.
 * @param {Error} err
 * @private
 */
JSONEditor.prototype._onError = function(err) {
    // TODO: onError is deprecated since version 2.2.0. cleanup some day
    if (typeof this.onError === 'function') {
        util.log('WARNING: JSONEditor.onError is deprecated. ' +
            'Use options.error instead.');
        this.onError(err);
    }

    if (this.options && typeof this.options.error === 'function') {
        this.options.error(err);
    }
    else {
        throw err;
    }
};

/**
 * @constructor TreeEditor
 * @param {Element} container    Container element
 * @param {Object}  [options]    Object with options. available options:
 *                               {String} mode      Editor mode. Available values:
 *                                                  'tree' (default), 'view',
 *                                                  and 'form'.
 *                               {Boolean} search   Enable search box.
 *                                                  True by default
 *                               {Boolean} history  Enable history (undo/redo).
 *                                                  True by default
 *                               {function} change  Callback method, triggered
 *                                                  on change of contents
 *                               {String} name      Field name for the root node.
 * @param {Object | undefined} json JSON object
 */
function TreeEditor(container, options, json) {
    if (!(this instanceof TreeEditor)) {
        throw new Error('TreeEditor constructor called without "new".');
    }

    this._create(container, options, json);
}

/**
 * Create the TreeEditor
 * @param {Element} container    Container element
 * @param {Object}  [options]    See description in constructor
 * @param {Object | undefined} json JSON object
 * @private
 */
TreeEditor.prototype._create = function (container, options, json) {
    // check availability of JSON parser (not available in IE7 and older)
    if (typeof(JSON) == 'undefined') {
        throw new Error ('Your browser does not support JSON. \n\n' +
            'Please install the newest version of your browser.\n' +
            '(all modern browsers support JSON).');
    }

    if (!container) {
        throw new Error('No container element provided.');
    }
    this.container = container;
    this.dom = {};
    this.highlighter = new Highlighter();
    this.selection = undefined; // will hold the last input selection

    this._setOptions(options);

    if (this.options.history && !this.mode.view) {
        this.history = new History(this);
    }

    this._createFrame();
    this._createTable();

    this.set(json || {});
};

/**
 * Detach the editor from the DOM
 * @private
 */
TreeEditor.prototype._delete = function () {
    if (this.frame && this.container && this.frame.parentNode == this.container) {
        this.container.removeChild(this.frame);
    }
};

/**
 * Initialize and set default options
 * @param {Object}  [options]    See description in constructor
 * @private
 */
TreeEditor.prototype._setOptions = function (options) {
    this.options = {
        search: true,
        history: true,
        mode: 'tree',
        name: undefined   // field name of root node
    };

    // copy all options
    if (options) {
        for (var prop in options) {
            if (options.hasOwnProperty(prop)) {
                this.options[prop] = options[prop];
            }
        }

        // check for deprecated options
        if (options['enableSearch']) {
            // deprecated since version 1.6.0, 2012-11-03
            this.options.search = options['enableSearch'];
            util.log('WARNING: Option "enableSearch" is deprecated. Use "search" instead.');
        }
        if (options['enableHistory']) {
            // deprecated since version 1.6.0, 2012-11-03
            this.options.history = options['enableHistory'];
            util.log('WARNING: Option "enableHistory" is deprecated. Use "history" instead.');
        }
        if (options['mode'] == 'editor') {
            // deprecated since version 2.2.0, 2013-04-30
            this.options.mode = 'tree';
            util.log('WARNING: Mode "editor" is deprecated. Use "tree" instead.');
        }
        if (options['mode'] == 'viewer') {
            // deprecated since version 2.2.0, 2013-04-30
            this.options.mode = 'view';
            util.log('WARNING: Mode "viewer" is deprecated. Use "view" instead.');
        }
    }

    // interpret the mode options
    this.mode = {
        edit: (this.options.mode != 'view' && this.options.mode != 'form'),
        view: (this.options.mode == 'view'),
        form: (this.options.mode == 'form')
    };
};

// node currently being edited
TreeEditor.focusNode = undefined;

/**
 * Set JSON object in editor
 * @param {Object | undefined} json      JSON data
 * @param {String}             [name]    Optional field name for the root node.
 *                                       Can also be set using setName(name).
 */
TreeEditor.prototype.set = function (json, name) {
    // adjust field name for root node
    if (name) {
        // TODO: deprecated since version 2.2.0. Cleanup some day.
        util.log('Warning: second parameter "name" is deprecated. ' +
            'Use setName(name) instead.');
        this.options.name = name;
    }

    // verify if json is valid JSON, ignore when a function
    if (json instanceof Function || (json === undefined)) {
        this.clear();
    }
    else {
        this.content.removeChild(this.table);  // Take the table offline

        // replace the root node
        var params = {
            'field': this.options.name,
            'value': json
        };
        var node = new Node(this, params);
        this._setRoot(node);

        // expand
        var recurse = false;
        this.node.expand(recurse);

        this.content.appendChild(this.table);  // Put the table online again
    }

    // TODO: maintain history, store last state and previous document
    if (this.history) {
        this.history.clear();
    }
};

/**
 * Get JSON object from editor
 * @return {Object | undefined} json
 */
TreeEditor.prototype.get = function () {
    // remove focus from currently edited node
    if (TreeEditor.focusNode) {
        TreeEditor.focusNode.blur();
    }

    if (this.node) {
        return this.node.getValue();
    }
    else {
        return undefined;
    }
};

/**
 * Get the text contents of the TreeEditor
 * @return {String} jsonText
 */
TreeEditor.prototype.getText = function() {
    return JSON.stringify(this.get());
};

/**
 * Set the text contents of the TreeEditor
 * @param {String} jsonText
 */
TreeEditor.prototype.setText = function(jsonText) {
    this.set(util.parse(jsonText));
};

/**
 * Set a field name for the root node.
 * @param {String | undefined} name
 */
TreeEditor.prototype.setName = function (name) {
    this.options.name = name;
    if (this.node) {
        this.node.updateField(this.options.name);
    }
};

/**
 * Get the field name for the root node.
 * @return {String | undefined} name
 */
TreeEditor.prototype.getName = function () {
    return this.options.name;
};

/**
 * Remove the root node from the editor
 */
TreeEditor.prototype.clear = function () {
    if (this.node) {
        this.node.collapse();
        this.tbody.removeChild(this.node.getDom());
        delete this.node;
    }
};

/**
 * Set the root node for the json editor
 * @param {Node} node
 * @private
 */
TreeEditor.prototype._setRoot = function (node) {
    this.clear();

    this.node = node;

    // append to the dom
    this.tbody.appendChild(node.getDom());
};

/**
 * Search text in all nodes
 * The nodes will be expanded when the text is found one of its childs,
 * else it will be collapsed. Searches are case insensitive.
 * @param {String} text
 * @return {Object[]} results  Array with nodes containing the search results
 *                             The result objects contains fields:
 *                             - {Node} node,
 *                             - {String} elem  the dom element name where
 *                                              the result is found ('field' or
 *                                              'value')
 */
TreeEditor.prototype.search = function (text) {
    var results;
    if (this.node) {
        this.content.removeChild(this.table);  // Take the table offline
        results = this.node.search(text);
        this.content.appendChild(this.table);  // Put the table online again
    }
    else {
        results = [];
    }

    return results;
};

/**
 * Expand all nodes
 */
TreeEditor.prototype.expandAll = function () {
    if (this.node) {
        this.content.removeChild(this.table);  // Take the table offline
        this.node.expand();
        this.content.appendChild(this.table);  // Put the table online again
    }
};

/**
 * Collapse all nodes
 */
TreeEditor.prototype.collapseAll = function () {
    if (this.node) {
        this.content.removeChild(this.table);  // Take the table offline
        this.node.collapse();
        this.content.appendChild(this.table);  // Put the table online again
    }
};

/**
 * The method onChange is called whenever a field or value is changed, created,
 * deleted, duplicated, etc.
 * @param {String} action  Change action. Available values: "editField",
 *                         "editValue", "changeType", "appendNode",
 *                         "removeNode", "duplicateNode", "moveNode", "expand",
 *                         "collapse".
 * @param {Object} params  Object containing parameters describing the change.
 *                         The parameters in params depend on the action (for
 *                         example for "editValue" the Node, old value, and new
 *                         value are provided). params contains all information
 *                         needed to undo or redo the action.
 * @private
 */
TreeEditor.prototype._onAction = function (action, params) {
    // add an action to the history
    if (this.history) {
        this.history.add(action, params);
    }

    // trigger the onChange callback
    if (this.options.change) {
        try {
            this.options.change();
        }
        catch (err) {
            util.log('Error in change callback: ', err);
        }
    }
};

/**
 * Start autoscrolling when given mouse position is above the top of the
 * editor contents, or below the bottom.
 * @param {Number} mouseY  Absolute mouse position in pixels
 */
TreeEditor.prototype.startAutoScroll = function (mouseY) {
    var me = this;
    var content = this.content;
    var top = util.getAbsoluteTop(content);
    var height = content.clientHeight;
    var bottom = top + height;
    var margin = 24;
    var interval = 50; // ms

    if ((mouseY < top + margin) && content.scrollTop > 0) {
        this.autoScrollStep = ((top + margin) - mouseY) / 3;
    }
    else if (mouseY > bottom - margin &&
            height + content.scrollTop < content.scrollHeight) {
        this.autoScrollStep = ((bottom - margin) - mouseY) / 3;
    }
    else {
        this.autoScrollStep = undefined;
    }

    if (this.autoScrollStep) {
        if (!this.autoScrollTimer) {
            this.autoScrollTimer = setInterval(function () {
                if (me.autoScrollStep) {
                    content.scrollTop -= me.autoScrollStep;
                }
                else {
                    me.stopAutoScroll();
                }
            }, interval);
        }
    }
    else {
        this.stopAutoScroll();
    }
};

/**
 * Stop auto scrolling. Only applicable when scrolling
 */
TreeEditor.prototype.stopAutoScroll = function () {
    if (this.autoScrollTimer) {
        clearTimeout(this.autoScrollTimer);
        delete this.autoScrollTimer;
    }
    if (this.autoScrollStep) {
        delete this.autoScrollStep;
    }
};


/**
 * Set the focus to an element in the TreeEditor, set text selection, and
 * set scroll position.
 * @param {Object} selection  An object containing fields:
 *                            {Element | undefined} dom     The dom element
 *                                                          which has focus
 *                            {Range | TextRange} range     A text selection
 *                            {Number} scrollTop            Scroll position
 */
TreeEditor.prototype.setSelection = function (selection) {
    if (!selection) {
        return;
    }

    if ('scrollTop' in selection && this.content) {
        // TODO: animated scroll
        this.content.scrollTop = selection.scrollTop;
    }
    if (selection.range) {
        util.setSelectionOffset(selection.range);
    }
    if (selection.dom) {
        selection.dom.focus();
    }
};

/**
 * Get the current focus
 * @return {Object} selection An object containing fields:
 *                            {Element | undefined} dom     The dom element
 *                                                          which has focus
 *                            {Range | TextRange} range     A text selection
 *                            {Number} scrollTop            Scroll position
 */
TreeEditor.prototype.getSelection = function () {
    return {
        dom: TreeEditor.domFocus,
        scrollTop: this.content ? this.content.scrollTop : 0,
        range: util.getSelectionOffset()
    };
};

/**
 * Adjust the scroll position such that given top position is shown at 1/4
 * of the window height.
 * @param {Number} top
 * @param {function(boolean)} [callback]   Callback, executed when animation is
 *                                         finished. The callback returns true
 *                                         when animation is finished, or false
 *                                         when not.
 */
TreeEditor.prototype.scrollTo = function (top, callback) {
    var content = this.content;
    if (content) {
        var editor = this;
        // cancel any running animation
        if (editor.animateTimeout) {
            clearTimeout(editor.animateTimeout);
            delete editor.animateTimeout;
        }
        if (editor.animateCallback) {
            editor.animateCallback(false);
            delete editor.animateCallback;
        }

        // calculate final scroll position
        var height = content.clientHeight;
        var bottom = content.scrollHeight - height;
        var finalScrollTop = Math.min(Math.max(top - height / 4, 0), bottom);

        // animate towards the new scroll position
        var animate = function () {
            var scrollTop = content.scrollTop;
            var diff = (finalScrollTop - scrollTop);
            if (Math.abs(diff) > 3) {
                content.scrollTop += diff / 3;
                editor.animateCallback = callback;
                editor.animateTimeout = setTimeout(animate, 50);
            }
            else {
                // finished
                if (callback) {
                    callback(true);
                }
                content.scrollTop = finalScrollTop;
                delete editor.animateTimeout;
                delete editor.animateCallback;
            }
        };
        animate();
    }
    else {
        if (callback) {
            callback(false);
        }
    }
};

/**
 * Create main frame
 * @private
 */
TreeEditor.prototype._createFrame = function () {
    // create the frame
    this.frame = document.createElement('div');
    this.frame.className = 'jsoneditor';
    this.container.appendChild(this.frame);

    // create one global event listener to handle all events from all nodes
    var editor = this;
    var onEvent = function (event) {
        editor._onEvent(event);
    };
    this.frame.onclick = function (event) {
        event = event || window.event;
        var target = event.target || event.srcElement;

        onEvent(event);

        // prevent default submit action of buttons when TreeEditor is located
        // inside a form
        if (target.nodeName == 'BUTTON') {
            util.preventDefault(event);
        }
    };
    this.frame.oninput = onEvent;
    this.frame.onchange = onEvent;
    this.frame.onkeydown = onEvent;
    this.frame.onkeyup = onEvent;
    this.frame.oncut = onEvent;
    this.frame.onpaste = onEvent;
    this.frame.onmousedown = onEvent;
    this.frame.onmouseup = onEvent;
    this.frame.onmouseover = onEvent;
    this.frame.onmouseout = onEvent;
    // Note: focus and blur events do not propagate, therefore they defined
    // using an eventListener with useCapture=true
    // see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html
    util.addEventListener(this.frame, 'focus', onEvent, true);
    util.addEventListener(this.frame, 'blur', onEvent, true);
    this.frame.onfocusin = onEvent;  // for IE
    this.frame.onfocusout = onEvent; // for IE

    // create menu
    this.menu = document.createElement('div');
    this.menu.className = 'menu';
    this.frame.appendChild(this.menu);

    // create expand all button
    var expandAll = document.createElement('button');
    expandAll.className = 'expand-all';
    expandAll.title = 'Expand all fields';
    expandAll.onclick = function () {
        editor.expandAll();
    };
    this.menu.appendChild(expandAll);

    // create expand all button
    var collapseAll = document.createElement('button');
    collapseAll.title = 'Collapse all fields';
    collapseAll.className = 'collapse-all';
    collapseAll.onclick = function () {
        editor.collapseAll();
    };
    this.menu.appendChild(collapseAll);

    // create undo/redo buttons
    if (this.history) {
        // create undo button
        var undo = document.createElement('button');
        undo.className = 'undo separator';
        undo.title = 'Undo last action (Ctrl+Z)';
        undo.onclick = function () {
            editor._onUndo();
        };
        this.menu.appendChild(undo);
        this.dom.undo = undo;

        // create redo button
        var redo = document.createElement('button');
        redo.className = 'redo';
        redo.title = 'Redo (Ctrl+Shift+Z)';
        redo.onclick = function () {
            editor._onRedo();
        };
        this.menu.appendChild(redo);
        this.dom.redo = redo;

        // register handler for onchange of history
        this.history.onChange = function () {
            undo.disabled = !editor.history.canUndo();
            redo.disabled = !editor.history.canRedo();
        };
        this.history.onChange();
    }

    // create mode box
    if (this.options && this.options.modes && this.options.modes.length) {
        var modeBox = createModeBox(this, this.options.modes, this.options.mode);
        this.menu.appendChild(modeBox);
        this.dom.modeBox = modeBox;
    }

    // create search box
    if (this.options.search) {
        this.searchBox = new SearchBox(this, this.menu);
    }
};

/**
 * Perform an undo action
 * @private
 */
TreeEditor.prototype._onUndo = function () {
    if (this.history) {
        // undo last action
        this.history.undo();

        // trigger change callback
        if (this.options.change) {
            this.options.change();
        }
    }
};

/**
 * Perform a redo action
 * @private
 */
TreeEditor.prototype._onRedo = function () {
    if (this.history) {
        // redo last action
        this.history.redo();

        // trigger change callback
        if (this.options.change) {
            this.options.change();
        }
    }
};

/**
 * Event handler
 * @param event
 * @private
 */
TreeEditor.prototype._onEvent = function (event) {
    event = event || window.event;
    var target = event.target || event.srcElement;

    if (event.type == 'keydown') {
        this._onKeyDown(event);
    }

    if (event.type == 'focus') {
        TreeEditor.domFocus = target;
    }

    var node = Node.getNodeFromTarget(target);
    if (node) {
        node.onEvent(event);
    }
};

/**
 * Event handler for keydown. Handles shortcut keys
 * @param {Event} event
 * @private
 */
TreeEditor.prototype._onKeyDown = function (event) {
    var keynum = event.which || event.keyCode;
    var ctrlKey = event.ctrlKey;
    var shiftKey = event.shiftKey;
    var handled = false;

    if (keynum == 9) { // Tab or Shift+Tab
        // FIXME: selecting all text on tab key does not work on IE8 (-> put selectContentEditable() in keyup too?)
        //Node.select(TreeEditor.domFocus);
        setTimeout(function () {
            // select all text when moving focus to an editable div
            util.selectContentEditable(TreeEditor.domFocus);
        }, 0);
    }

    if (this.searchBox) {
        if (ctrlKey && keynum == 70) { // Ctrl+F
            this.searchBox.dom.search.focus();
            this.searchBox.dom.search.select();
            handled = true;
        }
        else if (keynum == 114 || (ctrlKey && keynum == 71)) { // F3 or Ctrl+G
            var focus = true;
            if (!shiftKey) {
                // select next search result (F3 or Ctrl+G)
                this.searchBox.next(focus);
            }
            else {
                // select previous search result (Shift+F3 or Ctrl+Shift+G)
                this.searchBox.previous(focus);
            }

            handled = true;
        }
    }

    if (this.history) {
        if (ctrlKey && !shiftKey && keynum == 90) { // Ctrl+Z
            // undo
            this._onUndo();
            handled = true;
        }
        else if (ctrlKey && shiftKey && keynum == 90) { // Ctrl+Shift+Z
            // redo
            this._onRedo();
            handled = true;
        }
    }

    if (handled) {
        util.preventDefault(event);
        util.stopPropagation(event);
    }
};

/**
 * Create main table
 * @private
 */
TreeEditor.prototype._createTable = function () {
    var contentOuter = document.createElement('div');
    contentOuter.className = 'outer';
    this.contentOuter = contentOuter;

    this.content = document.createElement('div');
    this.content.className = 'tree';
    contentOuter.appendChild(this.content);

    this.table = document.createElement('table');
    this.table.className = 'tree';
    this.content.appendChild(this.table);

    // IE8 does not handle overflow='auto' correctly.
    // Therefore, set overflow to 'scroll'
    var ieVersion = util.getInternetExplorerVersion();
    if (ieVersion == 8) {
        this.content.style.overflow = 'scroll';
    }

    // create colgroup where the first two columns don't have a fixed
    // width, and the edit columns do have a fixed width
    var col;
    this.colgroupContent = document.createElement('colgroup');
    if (this.mode.edit) {
        col = document.createElement('col');
        col.width = "24px";
        this.colgroupContent.appendChild(col);
    }
    col = document.createElement('col');
    col.width = "24px";
    this.colgroupContent.appendChild(col);
    col = document.createElement('col');
    this.colgroupContent.appendChild(col);
    this.table.appendChild(this.colgroupContent);

    this.tbody = document.createElement('tbody');
    this.table.appendChild(this.tbody);

    this.frame.appendChild(contentOuter);
};

// register modes at the JSONEditor
JSONEditor.modes.tree = {
    editor: TreeEditor,
    data: 'json'
};
JSONEditor.modes.view = {
    editor: TreeEditor,
    data: 'json'
};
JSONEditor.modes.form = {
    editor: TreeEditor,
    data: 'json'
};
// Deprecated modes (deprecated since version 2.2.0)
JSONEditor.modes.editor = {
    editor: TreeEditor,
    data: 'json'
};
JSONEditor.modes.viewer = {
    editor: TreeEditor,
    data: 'json'
};

/**
 * Create a TextEditor and attach it to given container
 * @constructor TextEditor
 * @param {Element} container
 * @param {Object} [options]         Object with options. available options:
 *                                   {String} mode         Available values:
 *                                                         "text" (default)
 *                                                         or "code".
 *                                   {Number} indentation  Number of indentation
 *                                                         spaces. 4 by default.
 *                                   {function} change     Callback method
 *                                                         triggered on change
 * @param {JSON | String} [json]     initial contents of the formatter
 */
function TextEditor(container, options, json) {
    if (!(this instanceof TextEditor)) {
        throw new Error('TextEditor constructor called without "new".');
    }

    this._create(container, options, json);
}

/**
 * Create a TextEditor and attach it to given container
 * @constructor TextEditor
 * @param {Element} container
 * @param {Object} [options]         See description in constructor
 * @param {JSON | String} [json]     initial contents of the formatter
 * @private
 */
TextEditor.prototype._create = function (container, options, json) {
    // check availability of JSON parser (not available in IE7 and older)
    if (typeof(JSON) == 'undefined') {
        throw new Error('Your browser does not support JSON. \n\n' +
            'Please install the newest version of your browser.\n' +
            '(all modern browsers support JSON).');
    }

    // read options
    options = options || {};
    this.options = options;
    if (options.indentation) {
        this.indentation = Number(options.indentation);
    }
    else {
        this.indentation = 4;       // number of spaces
    }
    this.mode = (options.mode == 'code') ? 'code' : 'text';
    if (this.mode == 'code') {
        // verify whether Ace editor is available and supported
        if (typeof ace === 'undefined') {
            this.mode = 'text';
            util.log('WARNING: Cannot load code editor, Ace library not loaded. ' +
                'Falling back to plain text editor');
        }
        if (util.getInternetExplorerVersion() == 8) {
            this.mode = 'text';
            util.log('WARNING: Cannot load code editor, Ace is not supported on IE8. ' +
                'Falling back to plain text editor');
        }
    }

    var me = this;
    this.container = container;
    this.dom = {};
    this.editor = undefined;    // ace code editor
    this.textarea = undefined;  // plain text editor (fallback when Ace is not available)

    this.width = container.clientWidth;
    this.height = container.clientHeight;

    this.frame = document.createElement('div');
    this.frame.className = 'jsoneditor';
    this.frame.onclick = function (event) {
        // prevent default submit action when TextEditor is located inside a form
        util.preventDefault(event);
    };

    // create menu
    this.menu = document.createElement('div');
    this.menu.className = 'menu';
    this.frame.appendChild(this.menu);

    // create format button
    var buttonFormat = document.createElement('button');
    buttonFormat.className = 'format';
    buttonFormat.title = 'Format JSON data, with proper indentation and line feeds';
    this.menu.appendChild(buttonFormat);
    buttonFormat.onclick = function () {
        try {
            me.format();
        }
        catch (err) {
            me._onError(err);
        }
    };

    // create compact button
    var buttonCompact = document.createElement('button');
    buttonCompact.className = 'compact';
    buttonCompact.title = 'Compact JSON data, remove all whitespaces';
    this.menu.appendChild(buttonCompact);
    buttonCompact.onclick = function () {
        try {
            me.compact();
        }
        catch (err) {
            me._onError(err);
        }
    };

    // create mode box
    if (this.options && this.options.modes && this.options.modes.length) {
        var modeBox = createModeBox(this, this.options.modes, this.options.mode);
        this.menu.appendChild(modeBox);
        this.dom.modeBox = modeBox;
    }

    this.content = document.createElement('div');
    this.content.className = 'outer';
    this.frame.appendChild(this.content);

    this.container.appendChild(this.frame);

    if (this.mode == 'code') {
        this.editorDom = document.createElement('div');
        this.editorDom.style.height = '100%'; // TODO: move to css
        this.editorDom.style.width = '100%'; // TODO: move to css
        this.content.appendChild(this.editorDom);

        var editor = ace.edit(this.editorDom);
        editor.setTheme('ace/theme/jsoneditor');
        editor.setShowPrintMargin(false);
        editor.setFontSize(13);
        editor.getSession().setMode('ace/mode/json');
        editor.getSession().setUseSoftTabs(true);
        editor.getSession().setUseWrapMode(true);
        this.editor = editor;

        if (options.change) {
            // register onchange event
            editor.on('change', function () {
                options.change();
            });
        }
    }
    else {
        // load a plain text textarea
        var textarea = document.createElement('textarea');
        textarea.className = 'text';
        textarea.spellcheck = false;
        this.content.appendChild(textarea);
        this.textarea = textarea;

        if (options.change) {
            // register onchange event
            if (this.textarea.oninput === null) {
                this.textarea.oninput = function () {
                    options.change();
                }
            }
            else {
                // oninput is undefined. For IE8-
                this.textarea.onchange = function () {
                    options.change();
                }
            }
        }
    }

    // load initial json object or string
    if (typeof(json) == 'string') {
        this.setText(json);
    }
    else {
        this.set(json);
    }
};

/**
 * Detach the editor from the DOM
 * @private
 */
TextEditor.prototype._delete = function () {
    if (this.frame && this.container && this.frame.parentNode == this.container) {
        this.container.removeChild(this.frame);
    }
};

/**
 * Throw an error. If an error callback is configured in options.error, this
 * callback will be invoked. Else, a regular error is thrown.
 * @param {Error} err
 * @private
 */
TextEditor.prototype._onError = function(err) {
    // TODO: onError is deprecated since version 2.2.0. cleanup some day
    if (typeof this.onError === 'function') {
        util.log('WARNING: JSONEditor.onError is deprecated. ' +
            'Use options.error instead.');
        this.onError(err);
    }

    if (this.options && typeof this.options.error === 'function') {
        this.options.error(err);
    }
    else {
        throw err;
    }
};

/**
 * Compact the code in the formatter
 */
TextEditor.prototype.compact = function () {
    var json = util.parse(this.getText());
    this.setText(JSON.stringify(json));
};

/**
 * Format the code in the formatter
 */
TextEditor.prototype.format = function () {
    var json = util.parse(this.getText());
    this.setText(JSON.stringify(json, null, this.indentation));
};

/**
 * Set focus to the formatter
 */
TextEditor.prototype.focus = function () {
    if (this.textarea) {
        this.textarea.focus();
    }
    if (this.editor) {
        this.editor.focus();
    }
};

/**
 * Resize the formatter
 */
TextEditor.prototype.resize = function () {
    if (this.editor) {
        var force = false;
        this.editor.resize(force);
    }
};

/**
 * Set json data in the formatter
 * @param {Object} json
 */
TextEditor.prototype.set = function(json) {
    this.setText(JSON.stringify(json, null, this.indentation));
};

/**
 * Get json data from the formatter
 * @return {Object} json
 */
TextEditor.prototype.get = function() {
    return util.parse(this.getText());
};

/**
 * Get the text contents of the TextEditor
 * @return {String} jsonText
 */
TextEditor.prototype.getText = function() {
    if (this.textarea) {
        return this.textarea.value;
    }
    if (this.editor) {
        return this.editor.getValue();
    }
    return '';
};

/**
 * Set the text contents of the TextEditor
 * @param {String} jsonText
 */
TextEditor.prototype.setText = function(jsonText) {
    if (this.textarea) {
        this.textarea.value = jsonText;
    }
    if (this.editor) {
        this.editor.setValue(jsonText, -1);
    }
};

// register modes at the JSONEditor
JSONEditor.modes.text = {
    editor: TextEditor,
    data: 'text',
    load: TextEditor.prototype.format
};
JSONEditor.modes.code = {
    editor: TextEditor,
    data: 'text',
    load: TextEditor.prototype.format
};

/**
 * @constructor Node
 * Create a new Node
 * @param {TreeEditor} editor
 * @param {Object} [params] Can contain parameters:
 *                          {string}  field
 *                          {boolean} fieldEditable
 *                          {*}       value
 *                          {String}  type  Can have values 'auto', 'array',
 *                                          'object', or 'string'.
 */
function Node (editor, params) {
    /** @type {TreeEditor} */
    this.editor = editor;
    this.dom = {};
    this.expanded = false;

    if(params && (params instanceof Object)) {
        this.setField(params.field, params.fieldEditable);
        this.setValue(params.value, params.type);
    }
    else {
        this.setField('');
        this.setValue(null);
    }
};

/**
 * Set parent node
 * @param {Node} parent
 */
Node.prototype.setParent = function(parent) {
    this.parent = parent;
};

/**
 * Set field
 * @param {String}  field
 * @param {boolean} [fieldEditable]
 */
Node.prototype.setField = function(field, fieldEditable) {
    this.field = field;
    this.fieldEditable = (fieldEditable == true);
};

/**
 * Get field
 * @return {String}
 */
Node.prototype.getField = function() {
    if (this.field === undefined) {
        this._getDomField();
    }

    return this.field;
};

/**
 * Set value. Value is a JSON structure or an element String, Boolean, etc.
 * @param {*} value
 * @param {String} [type]  Specify the type of the value. Can be 'auto',
 *                         'array', 'object', or 'string'
 */
Node.prototype.setValue = function(value, type) {
    var childValue, child;

    // first clear all current childs (if any)
    var childs = this.childs;
    if (childs) {
        while (childs.length) {
            this.removeChild(childs[0]);
        }
    }

    // TODO: remove the DOM of this Node

    this.type = this._getType(value);

    // check if type corresponds with the provided type
    if (type && type != this.type) {
        if (type == 'string' && this.type == 'auto') {
            this.type = type;
        }
        else {
            throw new Error('Type mismatch: ' +
                'cannot cast value of type "' + this.type +
                ' to the specified type "' + type + '"');
        }
    }

    if (this.type == 'array') {
        // array
        this.childs = [];
        for (var i = 0, iMax = value.length; i < iMax; i++) {
            childValue = value[i];
            if (childValue !== undefined && !(childValue instanceof Function)) {
                // ignore undefined and functions
                child = new Node(this.editor, {
                    'value': childValue
                });
                this.appendChild(child);
            }
        }
        this.value = '';
    }
    else if (this.type == 'object') {
        // object
        this.childs = [];
        for (var childField in value) {
            if (value.hasOwnProperty(childField)) {
                childValue = value[childField];
                if (childValue !== undefined && !(childValue instanceof Function)) {
                    // ignore undefined and functions
                    child = new Node(this.editor, {
                        'field': childField,
                        'value': childValue
                    });
                    this.appendChild(child);
                }
            }
        }
        this.value = '';
    }
    else {
        // value
        this.childs = undefined;
        this.value = value;
        /* TODO
         if (typeof(value) == 'string') {
         var escValue = JSON.stringify(value);
         this.value = escValue.substring(1, escValue.length - 1);
         util.log('check', value, this.value);
         }
         else {
         this.value = value;
         }
         */
    }
};

/**
 * Get value. Value is a JSON structure
 * @return {*} value
 */
Node.prototype.getValue = function() {
    //var childs, i, iMax;

    if (this.type == 'array') {
        var arr = [];
        this.childs.forEach (function (child) {
            arr.push(child.getValue());
        });
        return arr;
    }
    else if (this.type == 'object') {
        var obj = {};
        this.childs.forEach (function (child) {
            obj[child.getField()] = child.getValue();
        });
        return obj;
    }
    else {
        if (this.value === undefined) {
            this._getDomValue();
        }

        return this.value;
    }
};

/**
 * Get the nesting level of this node
 * @return {Number} level
 */
Node.prototype.getLevel = function() {
    return (this.parent ? this.parent.getLevel() + 1 : 0);
};

/**
 * Create a clone of a node
 * The complete state of a clone is copied, including whether it is expanded or
 * not. The DOM elements are not cloned.
 * @return {Node} clone
 */
Node.prototype.clone = function() {
    var clone = new Node(this.editor);
    clone.type = this.type;
    clone.field = this.field;
    clone.fieldInnerText = this.fieldInnerText;
    clone.fieldEditable = this.fieldEditable;
    clone.value = this.value;
    clone.valueInnerText = this.valueInnerText;
    clone.expanded = this.expanded;

    if (this.childs) {
        // an object or array
        var cloneChilds = [];
        this.childs.forEach(function (child) {
            var childClone = child.clone();
            childClone.setParent(clone);
            cloneChilds.push(childClone);
        });
        clone.childs = cloneChilds;
    }
    else {
        // a value
        clone.childs = undefined;
    }

    return clone;
};

/**
 * Expand this node and optionally its childs.
 * @param {boolean} [recurse] Optional recursion, true by default. When
 *                            true, all childs will be expanded recursively
 */
Node.prototype.expand = function(recurse) {
    if (!this.childs) {
        return;
    }

    // set this node expanded
    this.expanded = true;
    if (this.dom.expand) {
        this.dom.expand.className = 'expanded';
    }

    this.showChilds();

    if (recurse != false) {
        this.childs.forEach(function (child) {
            child.expand(recurse);
        });
    }
};

/**
 * Collapse this node and optionally its childs.
 * @param {boolean} [recurse] Optional recursion, true by default. When
 *                            true, all childs will be collapsed recursively
 */
Node.prototype.collapse = function(recurse) {
    if (!this.childs) {
        return;
    }

    this.hideChilds();

    // collapse childs in case of recurse
    if (recurse != false) {
        this.childs.forEach(function (child) {
            child.collapse(recurse);
        });

    }

    // make this node collapsed
    if (this.dom.expand) {
        this.dom.expand.className = 'collapsed';
    }
    this.expanded = false;
};

/**
 * Recursively show all childs when they are expanded
 */
Node.prototype.showChilds = function() {
    var childs = this.childs;
    if (!childs) {
        return;
    }
    if (!this.expanded) {
        return;
    }

    var tr = this.dom.tr;
    var table = tr ? tr.parentNode : undefined;
    if (table) {
        // show row with append button
        var append = this.getAppend();
        var nextTr = tr.nextSibling;
        if (nextTr) {
            table.insertBefore(append, nextTr);
        }
        else {
            table.appendChild(append);
        }

        // show childs
        this.childs.forEach(function (child) {
            table.insertBefore(child.getDom(), append);
            child.showChilds();
        });
    }
};

/**
 * Hide the node with all its childs
 */
Node.prototype.hide = function() {
    var tr = this.dom.tr;
    var table = tr ? tr.parentNode : undefined;
    if (table) {
        table.removeChild(tr);
    }
    this.hideChilds();
};


/**
 * Recursively hide all childs
 */
Node.prototype.hideChilds = function() {
    var childs = this.childs;
    if (!childs) {
        return;
    }
    if (!this.expanded) {
        return;
    }

    // hide append row
    var append = this.getAppend();
    if (append.parentNode) {
        append.parentNode.removeChild(append);
    }

    // hide childs
    this.childs.forEach(function (child) {
        child.hide();
    });
};


/**
 * Add a new child to the node.
 * Only applicable when Node value is of type array or object
 * @param {Node} node
 */
Node.prototype.appendChild = function(node) {
    if (this._hasChilds()) {
        // adjust the link to the parent
        node.setParent(this);
        node.fieldEditable = (this.type == 'object');
        if (this.type == 'array') {
            node.index = this.childs.length;
        }
        this.childs.push(node);

        if (this.expanded) {
            // insert into the DOM, before the appendRow
            var newTr = node.getDom();
            var appendTr = this.getAppend();
            var table = appendTr ? appendTr.parentNode : undefined;
            if (appendTr && table) {
                table.insertBefore(newTr, appendTr);
            }

            node.showChilds();
        }

        this.updateDom({'updateIndexes': true});
        node.updateDom({'recurse': true});
    }
};


/**
 * Move a node from its current parent to this node
 * Only applicable when Node value is of type array or object
 * @param {Node} node
 * @param {Node} beforeNode
 */
Node.prototype.moveBefore = function(node, beforeNode) {
    if (this._hasChilds()) {
        // create a temporary row, to prevent the scroll position from jumping
        // when removing the node
        var tbody = (this.dom.tr) ? this.dom.tr.parentNode : undefined;
        if (tbody) {
            var trTemp = document.createElement('tr');
            trTemp.style.height = tbody.clientHeight + 'px';
            tbody.appendChild(trTemp);
        }

        if (node.parent) {
            node.parent.removeChild(node);
        }

        if (beforeNode instanceof AppendNode) {
            this.appendChild(node);
        }
        else {
            this.insertBefore(node, beforeNode);
        }

        if (tbody) {
            tbody.removeChild(trTemp);
        }
    }
};

/**
 * Move a node from its current parent to this node
 * Only applicable when Node value is of type array or object.
 * If index is out of range, the node will be appended to the end
 * @param {Node} node
 * @param {Number} index
 */
Node.prototype.moveTo = function (node, index) {
    if (node.parent == this) {
        // same parent
        var currentIndex = this.childs.indexOf(node);
        if (currentIndex < index) {
            // compensate the index for removal of the node itself
            index++;
        }
    }

    var beforeNode = this.childs[index] || this.append;
    this.moveBefore(node, beforeNode);
};

/**
 * Insert a new child before a given node
 * Only applicable when Node value is of type array or object
 * @param {Node} node
 * @param {Node} beforeNode
 */
Node.prototype.insertBefore = function(node, beforeNode) {
    if (this._hasChilds()) {
        if (beforeNode == this.append) {
            // append to the child nodes

            // adjust the link to the parent
            node.setParent(this);
            node.fieldEditable = (this.type == 'object');
            this.childs.push(node);
        }
        else {
            // insert before a child node
            var index = this.childs.indexOf(beforeNode);
            if (index == -1) {
                throw new Error('Node not found');
            }

            // adjust the link to the parent
            node.setParent(this);
            node.fieldEditable = (this.type == 'object');
            this.childs.splice(index, 0, node);
        }

        if (this.expanded) {
            // insert into the DOM
            var newTr = node.getDom();
            var nextTr = beforeNode.getDom();
            var table = nextTr ? nextTr.parentNode : undefined;
            if (nextTr && table) {
                table.insertBefore(newTr, nextTr);
            }

            node.showChilds();
        }

        this.updateDom({'updateIndexes': true});
        node.updateDom({'recurse': true});
    }
};

/**
 * Insert a new child before a given node
 * Only applicable when Node value is of type array or object
 * @param {Node} node
 * @param {Node} afterNode
 */
Node.prototype.insertAfter = function(node, afterNode) {
    if (this._hasChilds()) {
        var index = this.childs.indexOf(afterNode);
        var beforeNode = this.childs[index + 1];
        if (beforeNode) {
            this.insertBefore(node, beforeNode);
        }
        else {
            this.appendChild(node);
        }
    }
};

/**
 * Search in this node
 * The node will be expanded when the text is found one of its childs, else
 * it will be collapsed. Searches are case insensitive.
 * @param {String} text
 * @return {Node[]} results  Array with nodes containing the search text
 */
Node.prototype.search = function(text) {
    var results = [];
    var index;
    var search = text ? text.toLowerCase() : undefined;

    // delete old search data
    delete this.searchField;
    delete this.searchValue;

    // search in field
    if (this.field != undefined) {
        var field = String(this.field).toLowerCase();
        index = field.indexOf(search);
        if (index != -1) {
            this.searchField = true;
            results.push({
                'node': this,
                'elem': 'field'
            });
        }

        // update dom
        this._updateDomField();
    }

    // search in value
    if (this._hasChilds()) {
        // array, object

        // search the nodes childs
        if (this.childs) {
            var childResults = [];
            this.childs.forEach(function (child) {
                childResults = childResults.concat(child.search(text));
            });
            results = results.concat(childResults);
        }

        // update dom
        if (search != undefined) {
            var recurse = false;
            if (childResults.length == 0) {
                this.collapse(recurse);
            }
            else {
                this.expand(recurse);
            }
        }
    }
    else {
        // string, auto
        if (this.value != undefined ) {
            var value = String(this.value).toLowerCase();
            index = value.indexOf(search);
            if (index != -1) {
                this.searchValue = true;
                results.push({
                    'node': this,
                    'elem': 'value'
                });
            }
        }

        // update dom
        this._updateDomValue();
    }

    return results;
};

/**
 * Move the scroll position such that this node is in the visible area.
 * The node will not get the focus
 * @param {function(boolean)} [callback]
 */
Node.prototype.scrollTo = function(callback) {
    if (!this.dom.tr || !this.dom.tr.parentNode) {
        // if the node is not visible, expand its parents
        var parent = this.parent;
        var recurse = false;
        while (parent) {
            parent.expand(recurse);
            parent = parent.parent;
        }
    }

    if (this.dom.tr && this.dom.tr.parentNode) {
        this.editor.scrollTo(this.dom.tr.offsetTop, callback);
    }
};


// stores the element name currently having the focus
Node.focusElement = undefined;

/**
 * Set focus to this node
 * @param {String} [elementName]  The field name of the element to get the
 *                                focus available values: 'drag', 'menu',
 *                                'expand', 'field', 'value' (default)
 */
Node.prototype.focus = function(elementName) {
    Node.focusElement = elementName;

    if (this.dom.tr && this.dom.tr.parentNode) {
        var dom = this.dom;

        switch (elementName) {
            case 'drag':
                if (dom.drag) {
                    dom.drag.focus();
                }
                else {
                    dom.menu.focus();
                }
                break;

            case 'menu':
                dom.menu.focus();
                break;

            case 'expand':
                if (this._hasChilds()) {
                    dom.expand.focus();
                }
                else if (dom.field && this.fieldEditable) {
                    dom.field.focus();
                    util.selectContentEditable(dom.field);
                }
                else if (dom.value && !this._hasChilds()) {
                    dom.value.focus();
                    util.selectContentEditable(dom.value);
                }
                else {
                    dom.menu.focus();
                }
                break;

            case 'field':
                if (dom.field && this.fieldEditable) {
                    dom.field.focus();
                    util.selectContentEditable(dom.field);
                }
                else if (dom.value && !this._hasChilds()) {
                    dom.value.focus();
                    util.selectContentEditable(dom.value);
                }
                else if (this._hasChilds()) {
                    dom.expand.focus();
                }
                else {
                    dom.menu.focus();
                }
                break;

            case 'value':
            default:
                if (dom.value && !this._hasChilds()) {
                    dom.value.focus();
                    util.selectContentEditable(dom.value);
                }
                else if (dom.field && this.fieldEditable) {
                    dom.field.focus();
                    util.selectContentEditable(dom.field);
                }
                else if (this._hasChilds()) {
                    dom.expand.focus();
                }
                else {
                    dom.menu.focus();
                }
                break;
        }
    }
};

/**
 * Select all text in an editable div after a delay of 0 ms
 * @param {Element} editableDiv
 */
Node.select = function(editableDiv) {
    setTimeout(function () {
        util.selectContentEditable(editableDiv);
    }, 0);
};

/**
 * Update the values from the DOM field and value of this node
 */
Node.prototype.blur = function() {
    // retrieve the actual field and value from the DOM.
    this._getDomValue(false);
    this._getDomField(false);
};

/**
 * Duplicate given child node
 * new structure will be added right before the cloned node
 * @param {Node} node           the childNode to be duplicated
 * @return {Node} clone         the clone of the node
 * @private
 */
Node.prototype._duplicate = function(node) {
    var clone = node.clone();

    /* TODO: adjust the field name (to prevent equal field names)
     if (this.type == 'object') {
     }
     */

    this.insertAfter(clone, node);

    return clone;
};

/**
 * Check if given node is a child. The method will check recursively to find
 * this node.
 * @param {Node} node
 * @return {boolean} containsNode
 */
Node.prototype.containsNode = function(node) {
    if (this == node) {
        return true;
    }

    var childs = this.childs;
    if (childs) {
        // TODO: use the js5 Array.some() here?
        for (var i = 0, iMax = childs.length; i < iMax; i++) {
            if (childs[i].containsNode(node)) {
                return true;
            }
        }
    }

    return false;
};

/**
 * Move given node into this node
 * @param {Node} node           the childNode to be moved
 * @param {Node} beforeNode     node will be inserted before given
 *                                         node. If no beforeNode is given,
 *                                         the node is appended at the end
 * @private
 */
Node.prototype._move = function(node, beforeNode) {
    if (node == beforeNode) {
        // nothing to do...
        return;
    }

    // check if this node is not a child of the node to be moved here
    if (node.containsNode(this)) {
        throw new Error('Cannot move a field into a child of itself');
    }

    // remove the original node
    if (node.parent) {
        node.parent.removeChild(node);
    }

    // create a clone of the node
    var clone = node.clone();
    node.clearDom();

    // insert or append the node
    if (beforeNode) {
        this.insertBefore(clone, beforeNode);
    }
    else {
        this.appendChild(clone);
    }

    /* TODO: adjust the field name (to prevent equal field names)
     if (this.type == 'object') {
     }
     */
};

/**
 * Remove a child from the node.
 * Only applicable when Node value is of type array or object
 * @param {Node} node   The child node to be removed;
 * @return {Node | undefined} node  The removed node on success,
 *                                             else undefined
 */
Node.prototype.removeChild = function(node) {
    if (this.childs) {
        var index = this.childs.indexOf(node);

        if (index != -1) {
            node.hide();

            // delete old search results
            delete node.searchField;
            delete node.searchValue;

            var removedNode = this.childs.splice(index, 1)[0];

            this.updateDom({'updateIndexes': true});

            return removedNode;
        }
    }

    return undefined;
};

/**
 * Remove a child node node from this node
 * This method is equal to Node.removeChild, except that _remove firex an
 * onChange event.
 * @param {Node} node
 * @private
 */
Node.prototype._remove = function (node) {
    this.removeChild(node);
};

/**
 * Change the type of the value of this Node
 * @param {String} newType
 */
Node.prototype.changeType = function (newType) {
    var oldType = this.type;

    if (oldType == newType) {
        // type is not changed
        return;
    }

    if ((newType == 'string' || newType == 'auto') &&
        (oldType == 'string' || oldType == 'auto')) {
        // this is an easy change
        this.type = newType;
    }
    else {
        // change from array to object, or from string/auto to object/array
        var table = this.dom.tr ? this.dom.tr.parentNode : undefined;
        var lastTr;
        if (this.expanded) {
            lastTr = this.getAppend();
        }
        else {
            lastTr = this.getDom();
        }
        var nextTr = (lastTr && lastTr.parentNode) ? lastTr.nextSibling : undefined;

        // hide current field and all its childs
        this.hide();
        this.clearDom();

        // adjust the field and the value
        this.type = newType;

        // adjust childs
        if (newType == 'object') {
            if (!this.childs) {
                this.childs = [];
            }

            this.childs.forEach(function (child, index) {
                child.clearDom();
                delete child.index;
                child.fieldEditable = true;
                if (child.field == undefined) {
                    child.field = '';
                }
            });

            if (oldType == 'string' || oldType == 'auto') {
                this.expanded = true;
            }
        }
        else if (newType == 'array') {
            if (!this.childs) {
                this.childs = [];
            }

            this.childs.forEach(function (child, index) {
                child.clearDom();
                child.fieldEditable = false;
                child.index = index;
            });

            if (oldType == 'string' || oldType == 'auto') {
                this.expanded = true;
            }
        }
        else {
            this.expanded = false;
        }

        // create new DOM
        if (table) {
            if (nextTr) {
                table.insertBefore(this.getDom(), nextTr);
            }
            else {
                table.appendChild(this.getDom());
            }
        }
        this.showChilds();
    }

    if (newType == 'auto' || newType == 'string') {
        // cast value to the correct type
        if (newType == 'string') {
            this.value = String(this.value);
        }
        else {
            this.value = this._stringCast(String(this.value));
        }

        this.focus();
    }

    this.updateDom({'updateIndexes': true});
};

/**
 * Retrieve value from DOM
 * @param {boolean} [silent]  If true (default), no errors will be thrown in
 *                            case of invalid data
 * @private
 */
Node.prototype._getDomValue = function(silent) {
    if (this.dom.value && this.type != 'array' && this.type != 'object') {
        this.valueInnerText = util.getInnerText(this.dom.value);
    }

    if (this.valueInnerText != undefined) {
        try {
            // retrieve the value
            var value;
            if (this.type == 'string') {
                value = this._unescapeHTML(this.valueInnerText);
            }
            else {
                var str = this._unescapeHTML(this.valueInnerText);
                value = this._stringCast(str);
            }
            if (value !== this.value) {
                var oldValue = this.value;
                this.value = value;
                this.editor._onAction('editValue', {
                    'node': this,
                    'oldValue': oldValue,
                    'newValue': value,
                    'oldSelection': this.editor.selection,
                    'newSelection': this.editor.getSelection()
                });
            }
        }
        catch (err) {
            this.value = undefined;
            // TODO: sent an action with the new, invalid value?
            if (silent != true) {
                throw err;
            }
        }
    }
};

/**
 * Update dom value:
 * - the text color of the value, depending on the type of the value
 * - the height of the field, depending on the width
 * - background color in case it is empty
 * @private
 */
Node.prototype._updateDomValue = function () {
    var domValue = this.dom.value;
    if (domValue) {
        // set text color depending on value type
        // TODO: put colors in css
        var v = this.value;
        var t = (this.type == 'auto') ? util.type(v) : this.type;
        var isUrl = (t == 'string' && util.isUrl(v));
        var color = '';
        if (isUrl && !this.editor.mode.edit) {
            color = '';
        }
        else if (t == 'string') {
            color = 'green';
        }
        else if (t == 'number') {
            color = 'red';
        }
        else if (t == 'boolean') {
            color = 'darkorange';
        }
        else if (this._hasChilds()) {
            color = '';
        }
        else if (v === null) {
            color = '#004ED0';  // blue
        }
        else {
            // invalid value
            color = 'black';
        }
        domValue.style.color = color;

        // make background color light-gray when empty
        var isEmpty = (String(this.value) == '' && this.type != 'array' && this.type != 'object');
        if (isEmpty) {
            util.addClassName(domValue, 'empty');
        }
        else {
            util.removeClassName(domValue, 'empty');
        }

        // underline url
        if (isUrl) {
            util.addClassName(domValue, 'url');
        }
        else {
            util.removeClassName(domValue, 'url');
        }

        // update title
        if (t == 'array' || t == 'object') {
            var count = this.childs ? this.childs.length : 0;
            domValue.title = this.type + ' containing ' + count + ' items';
        }
        else if (t == 'string' && util.isUrl(v)) {
            if (this.editor.mode.edit) {
                domValue.title = 'Ctrl+Click or Ctrl+Enter to open url in new window';
            }
        }
        else {
            domValue.title = '';
        }

        // highlight when there is a search result
        if (this.searchValueActive) {
            util.addClassName(domValue, 'highlight-active');
        }
        else {
            util.removeClassName(domValue, 'highlight-active');
        }
        if (this.searchValue) {
            util.addClassName(domValue, 'highlight');
        }
        else {
            util.removeClassName(domValue, 'highlight');
        }

        // strip formatting from the contents of the editable div
        util.stripFormatting(domValue);
    }
};

/**
 * Update dom field:
 * - the text color of the field, depending on the text
 * - the height of the field, depending on the width
 * - background color in case it is empty
 * @private
 */
Node.prototype._updateDomField = function () {
    var domField = this.dom.field;
    if (domField) {
        // make backgound color lightgray when empty
        var isEmpty = (String(this.field) == '' && this.parent.type != 'array');
        if (isEmpty) {
            util.addClassName(domField, 'empty');
        }
        else {
            util.removeClassName(domField, 'empty');
        }

        // highlight when there is a search result
        if (this.searchFieldActive) {
            util.addClassName(domField, 'highlight-active');
        }
        else {
            util.removeClassName(domField, 'highlight-active');
        }
        if (this.searchField) {
            util.addClassName(domField, 'highlight');
        }
        else {
            util.removeClassName(domField, 'highlight');
        }

        // strip formatting from the contents of the editable div
        util.stripFormatting(domField);
    }
};

/**
 * Retrieve field from DOM
 * @param {boolean} [silent]  If true (default), no errors will be thrown in
 *                            case of invalid data
 * @private
 */
Node.prototype._getDomField = function(silent) {
    if (this.dom.field && this.fieldEditable) {
        this.fieldInnerText = util.getInnerText(this.dom.field);
    }

    if (this.fieldInnerText != undefined) {
        try {
            var field = this._unescapeHTML(this.fieldInnerText);

            if (field !== this.field) {
                var oldField = this.field;
                this.field = field;
                this.editor._onAction('editField', {
                    'node': this,
                    'oldValue': oldField,
                    'newValue': field,
                    'oldSelection': this.editor.selection,
                    'newSelection': this.editor.getSelection()
                });
            }
        }
        catch (err) {
            this.field = undefined;
            // TODO: sent an action here, with the new, invalid value?
            if (silent != true) {
                throw err;
            }
        }
    }
};

/**
 * Clear the dom of the node
 */
Node.prototype.clearDom = function() {
    // TODO: hide the node first?
    //this.hide();
    // TODO: recursively clear dom?

    this.dom = {};
};

/**
 * Get the HTML DOM TR element of the node.
 * The dom will be generated when not yet created
 * @return {Element} tr    HTML DOM TR Element
 */
Node.prototype.getDom = function() {
    var dom = this.dom;
    if (dom.tr) {
        return dom.tr;
    }

    // create row
    dom.tr = document.createElement('tr');
    dom.tr.node = this;

    if (this.editor.mode.edit) {
        // create draggable area
        var tdDrag = document.createElement('td');
        if (this.parent) {
            var domDrag = document.createElement('button');
            dom.drag = domDrag;
            domDrag.className = 'dragarea';
            domDrag.title = 'Drag to move this field (Alt+Shift+Arrows)';
            tdDrag.appendChild(domDrag);
        }
        dom.tr.appendChild(tdDrag);

        // create context menu
        var tdMenu = document.createElement('td');
        var menu = document.createElement('button');
        dom.menu = menu;
        menu.className = 'contextmenu';
        menu.title = 'Click to open the actions menu (Ctrl+M)';
        tdMenu.appendChild(dom.menu);
        dom.tr.appendChild(tdMenu);
    }

    // create tree and field
    var tdField = document.createElement('td');
    dom.tr.appendChild(tdField);
    dom.tree = this._createDomTree();
    tdField.appendChild(dom.tree);

    this.updateDom({'updateIndexes': true});

    return dom.tr;
};

/**
 * DragStart event, fired on mousedown on the dragarea at the left side of a Node
 * @param {Event} event
 * @private
 */
Node.prototype._onDragStart = function (event) {
    event = event || window.event;

    var node = this;
    if (!this.mousemove) {
        this.mousemove = util.addEventListener(document, 'mousemove',
            function (event) {
                node._onDrag(event);
            });
    }

    if (!this.mouseup) {
        this.mouseup = util.addEventListener(document, 'mouseup',
            function (event ) {
                node._onDragEnd(event);
            });
    }

    this.editor.highlighter.lock();
    this.drag = {
        'oldCursor': document.body.style.cursor,
        'startParent': this.parent,
        'startIndex': this.parent.childs.indexOf(this),
        'mouseX': util.getMouseX(event),
        'level': this.getLevel()
    };
    document.body.style.cursor = 'move';

    util.preventDefault(event);
};

/**
 * Drag event, fired when moving the mouse while dragging a Node
 * @param {Event} event
 * @private
 */
Node.prototype._onDrag = function (event) {
    // TODO: this method has grown too large. Split it in a number of methods
    event = event || window.event;
    var mouseY = util.getMouseY(event);
    var mouseX = util.getMouseX(event);

    var trThis, trPrev, trNext, trFirst, trLast, trRoot;
    var nodePrev, nodeNext;
    var topThis, topPrev, topFirst, heightThis, bottomNext, heightNext;
    var moved = false;

    // TODO: add an ESC option, which resets to the original position

    // move up/down
    trThis = this.dom.tr;
    topThis = util.getAbsoluteTop(trThis);
    heightThis = trThis.offsetHeight;
    if (mouseY < topThis) {
        // move up
        trPrev = trThis;
        do {
            trPrev = trPrev.previousSibling;
            nodePrev = Node.getNodeFromTarget(trPrev);
            topPrev = trPrev ? util.getAbsoluteTop(trPrev) : 0;
        }
        while (trPrev && mouseY < topPrev);

        if (nodePrev && !nodePrev.parent) {
            nodePrev = undefined;
        }

        if (!nodePrev) {
            // move to the first node
            trRoot = trThis.parentNode.firstChild;
            trPrev = trRoot ? trRoot.nextSibling : undefined;
            nodePrev = Node.getNodeFromTarget(trPrev);
            if (nodePrev == this) {
                nodePrev = undefined;
            }
        }

        if (nodePrev) {
            // check if mouseY is really inside the found node
            trPrev = nodePrev.dom.tr;
            topPrev = trPrev ? util.getAbsoluteTop(trPrev) : 0;
            if (mouseY > topPrev + heightThis) {
                nodePrev = undefined;
            }
        }

        if (nodePrev) {
            nodePrev.parent.moveBefore(this, nodePrev);
            moved = true;
        }
    }
    else {
        // move down
        trLast = (this.expanded && this.append) ? this.append.getDom() : this.dom.tr;
        trFirst = trLast ? trLast.nextSibling : undefined;
        if (trFirst) {
            topFirst = util.getAbsoluteTop(trFirst);
            trNext = trFirst;
            do {
                nodeNext = Node.getNodeFromTarget(trNext);
                if (trNext) {
                    bottomNext = trNext.nextSibling ?
                        util.getAbsoluteTop(trNext.nextSibling) : 0;
                    heightNext = trNext ? (bottomNext - topFirst) : 0;

                    if (nodeNext.parent.childs.length == 1 && nodeNext.parent.childs[0] == this) {
                        // We are about to remove the last child of this parent,
                        // which will make the parents appendNode visible.
                        topThis += 24 - 1;
                        // TODO: dangerous to suppose the height of the appendNode a constant of 24-1 px.
                    }
                }

                trNext = trNext.nextSibling;
            }
            while (trNext && mouseY > topThis + heightNext);

            if (nodeNext && nodeNext.parent) {
                // calculate the desired level
                var diffX = (mouseX - this.drag.mouseX);
                var diffLevel = Math.round(diffX / 24 / 2);
                var level = this.drag.level + diffLevel; // desired level
                var levelNext = nodeNext.getLevel();     // level to be

                // find the best fitting level (move upwards over the append nodes)
                trPrev = nodeNext.dom.tr.previousSibling;
                while (levelNext < level && trPrev) {
                    nodePrev = Node.getNodeFromTarget(trPrev);
                    if (nodePrev == this || nodePrev._isChildOf(this)) {
                        // neglect itself and its childs
                    }
                    else if (nodePrev instanceof AppendNode) {
                        var childs = nodePrev.parent.childs;
                        if (childs.length > 1 ||
                            (childs.length == 1 && childs[0] != this)) {
                            // non-visible append node of a list of childs
                            // consisting of not only this node (else the
                            // append node will change into a visible "empty"
                            // text when removing this node).
                            nodeNext = Node.getNodeFromTarget(trPrev);
                            levelNext = nodeNext.getLevel();
                        }
                        else {
                            break;
                        }
                    }
                    else {
                        break;
                    }

                    trPrev = trPrev.previousSibling;
                }

                // move the node when its position is changed
                if (trLast.nextSibling != nodeNext.dom.tr) {
                    nodeNext.parent.moveBefore(this, nodeNext);
                    moved = true;
                }
            }
        }
    }

    if (moved) {
        // update the dragging parameters when moved
        this.drag.mouseX = mouseX;
        this.drag.level = this.getLevel();
    }

    // auto scroll when hovering around the top of the editor
    this.editor.startAutoScroll(mouseY);

    util.preventDefault(event);
};

/**
 * Drag event, fired on mouseup after having dragged a node
 * @param {Event} event
 * @private
 */
Node.prototype._onDragEnd = function (event) {
    event = event || window.event;

    var params = {
        'node': this,
        'startParent': this.drag.startParent,
        'startIndex': this.drag.startIndex,
        'endParent': this.parent,
        'endIndex': this.parent.childs.indexOf(this)
    };
    if ((params.startParent != params.endParent) ||
        (params.startIndex != params.endIndex)) {
        // only register this action if the node is actually moved to another place
        this.editor._onAction('moveNode', params);
    }

    document.body.style.cursor = this.drag.oldCursor;
    this.editor.highlighter.unlock();
    delete this.drag;

    if (this.mousemove) {
        util.removeEventListener(document, 'mousemove', this.mousemove);
        delete this.mousemove;}
    if (this.mouseup) {
        util.removeEventListener(document, 'mouseup', this.mouseup);
        delete this.mouseup;
    }

    // Stop any running auto scroll
    this.editor.stopAutoScroll();

    util.preventDefault(event);
};

/**
 * Test if this node is a child of an other node
 * @param {Node} node
 * @return {boolean} isChild
 * @private
 */
Node.prototype._isChildOf = function (node) {
    var n = this.parent;
    while (n) {
        if (n == node) {
            return true;
        }
        n = n.parent;
    }

    return false;
};

/**
 * Create an editable field
 * @return {Element} domField
 * @private
 */
Node.prototype._createDomField = function () {
    return document.createElement('div');
};

/**
 * Set highlighting for this node and all its childs.
 * Only applied to the currently visible (expanded childs)
 * @param {boolean} highlight
 */
Node.prototype.setHighlight = function (highlight) {
    if (this.dom.tr) {
        this.dom.tr.className = (highlight ? 'highlight' : '');

        if (this.append) {
            this.append.setHighlight(highlight);
        }

        if (this.childs) {
            this.childs.forEach(function (child) {
                child.setHighlight(highlight);
            });
        }
    }
};

/**
 * Update the value of the node. Only primitive types are allowed, no Object
 * or Array is allowed.
 * @param {String | Number | Boolean | null} value
 */
Node.prototype.updateValue = function (value) {
    this.value = value;
    this.updateDom();
};

/**
 * Update the field of the node.
 * @param {String} field
 */
Node.prototype.updateField = function (field) {
    this.field = field;
    this.updateDom();
};

/**
 * Update the HTML DOM, optionally recursing through the childs
 * @param {Object} [options] Available parameters:
 *                          {boolean} [recurse]         If true, the
 *                          DOM of the childs will be updated recursively.
 *                          False by default.
 *                          {boolean} [updateIndexes]   If true, the childs
 *                          indexes of the node will be updated too. False by
 *                          default.
 */
Node.prototype.updateDom = function (options) {
    // update level indentation
    var domTree = this.dom.tree;
    if (domTree) {
        domTree.style.marginLeft = this.getLevel() * 24 + 'px';
    }

    // update field
    var domField = this.dom.field;
    if (domField) {
        if (this.fieldEditable == true) {
            // parent is an object
            domField.contentEditable = this.editor.mode.edit;
            domField.spellcheck = false;
            domField.className = 'field';
        }
        else {
            // parent is an array this is the root node
            domField.className = 'readonly';
        }

        var field;
        if (this.index != undefined) {
            field = this.index;
        }
        else if (this.field != undefined) {
            field = this.field;
        }
        else if (this._hasChilds()) {
            field = this.type;
        }
        else {
            field = '';
        }
        domField.innerHTML = this._escapeHTML(field);
    }

    // update value
    var domValue = this.dom.value;
    if (domValue) {
        var count = this.childs ? this.childs.length : 0;
        if (this.type == 'array') {
            domValue.innerHTML = '[' + count + ']';
        }
        else if (this.type == 'object') {
            domValue.innerHTML = '{' + count + '}';
        }
        else {
            domValue.innerHTML = this._escapeHTML(this.value);
        }
    }

    // update field and value
    this._updateDomField();
    this._updateDomValue();

    // update childs indexes
    if (options && options.updateIndexes == true) {
        // updateIndexes is true or undefined
        this._updateDomIndexes();
    }

    if (options && options.recurse == true) {
        // recurse is true or undefined. update childs recursively
        if (this.childs) {
            this.childs.forEach(function (child) {
                child.updateDom(options);
            });
        }
    }

    // update row with append button
    if (this.append) {
        this.append.updateDom();
    }
};

/**
 * Update the DOM of the childs of a node: update indexes and undefined field
 * names.
 * Only applicable when structure is an array or object
 * @private
 */
Node.prototype._updateDomIndexes = function () {
    var domValue = this.dom.value;
    var childs = this.childs;
    if (domValue && childs) {
        if (this.type == 'array') {
            childs.forEach(function (child, index) {
                child.index = index;
                var childField = child.dom.field;
                if (childField) {
                    childField.innerHTML = index;
                }
            });
        }
        else if (this.type == 'object') {
            childs.forEach(function (child) {
                if (child.index != undefined) {
                    delete child.index;

                    if (child.field == undefined) {
                        child.field = '';
                    }
                }
            });
        }
    }
};

/**
 * Create an editable value
 * @private
 */
Node.prototype._createDomValue = function () {
    var domValue;

    if (this.type == 'array') {
        domValue = document.createElement('div');
        domValue.className = 'readonly';
        domValue.innerHTML = '[...]';
    }
    else if (this.type == 'object') {
        domValue = document.createElement('div');
        domValue.className = 'readonly';
        domValue.innerHTML = '{...}';
    }
    else {
        if (!this.editor.mode.edit && util.isUrl(this.value)) {
            // create a link in case of read-only editor and value containing an url
            domValue = document.createElement('a');
            domValue.className = 'value';
            domValue.href = this.value;
            domValue.target = '_blank';
            domValue.innerHTML = this._escapeHTML(this.value);
        }
        else {
            // create and editable or read-only div
            domValue = document.createElement('div');
            domValue.contentEditable = !this.editor.mode.view;
            domValue.spellcheck = false;
            domValue.className = 'value';
            domValue.innerHTML = this._escapeHTML(this.value);
        }
    }

    return domValue;
};

/**
 * Create an expand/collapse button
 * @return {Element} expand
 * @private
 */
Node.prototype._createDomExpandButton = function () {
    // create expand button
    var expand = document.createElement('button');
    if (this._hasChilds()) {
        expand.className = this.expanded ? 'expanded' : 'collapsed';
        expand.title =
            'Click to expand/collapse this field (Ctrl+E). \n' +
                'Ctrl+Click to expand/collapse including all childs.';
    }
    else {
        expand.className = 'invisible';
        expand.title = '';
    }

    return expand;
};


/**
 * Create a DOM tree element, containing the expand/collapse button
 * @return {Element} domTree
 * @private
 */
Node.prototype._createDomTree = function () {
    var dom = this.dom;
    var domTree = document.createElement('table');
    var tbody = document.createElement('tbody');
    domTree.style.borderCollapse = 'collapse'; // TODO: put in css
    domTree.className = 'values';
    domTree.appendChild(tbody);
    var tr = document.createElement('tr');
    tbody.appendChild(tr);

    // create expand button
    var tdExpand = document.createElement('td');
    tdExpand.className = 'tree';
    tr.appendChild(tdExpand);
    dom.expand = this._createDomExpandButton();
    tdExpand.appendChild(dom.expand);
    dom.tdExpand = tdExpand;

    // create the field
    var tdField = document.createElement('td');
    tdField.className = 'tree';
    tr.appendChild(tdField);
    dom.field = this._createDomField();
    tdField.appendChild(dom.field);
    dom.tdField = tdField;

    // create a separator
    var tdSeparator = document.createElement('td');
    tdSeparator.className = 'tree';
    tr.appendChild(tdSeparator);
    if (this.type != 'object' && this.type != 'array') {
        tdSeparator.appendChild(document.createTextNode(':'));
        tdSeparator.className = 'separator';
    }
    dom.tdSeparator = tdSeparator;

    // create the value
    var tdValue = document.createElement('td');
    tdValue.className = 'tree';
    tr.appendChild(tdValue);
    dom.value = this._createDomValue();
    tdValue.appendChild(dom.value);
    dom.tdValue = tdValue;

    return domTree;
};

/**
 * Handle an event. The event is catched centrally by the editor
 * @param {Event} event
 */
Node.prototype.onEvent = function (event) {
    var type = event.type,
        target = event.target || event.srcElement,
        dom = this.dom,
        node = this,
        focusNode,
        expandable = this._hasChilds();

    // check if mouse is on menu or on dragarea.
    // If so, highlight current row and its childs
    if (target == dom.drag || target == dom.menu) {
        if (type == 'mouseover') {
            this.editor.highlighter.highlight(this);
        }
        else if (type == 'mouseout') {
            this.editor.highlighter.unhighlight();
        }
    }

    // drag events
    if (type == 'mousedown' && target == dom.drag) {
        this._onDragStart(event);
    }

    // context menu events
    if (type == 'click' && target == dom.menu) {
        var highlighter = node.editor.highlighter;
        highlighter.highlight(node);
        highlighter.lock();
        util.addClassName(dom.menu, 'selected');
        this.showContextMenu(dom.menu, function () {
            util.removeClassName(dom.menu, 'selected');
            highlighter.unlock();
            highlighter.unhighlight();
        });
    }

    // expand events
    if (type == 'click' && target == dom.expand) {
        if (expandable) {
            var recurse = event.ctrlKey; // with ctrl-key, expand/collapse all
            this._onExpand(recurse);
        }
    }

    // value events
    var domValue = dom.value;
    if (target == domValue) {
        //noinspection FallthroughInSwitchStatementJS
        switch (type) {
            case 'focus':
                focusNode = this;
                break;

            case 'blur':
            case 'change':
                this._getDomValue(true);
                this._updateDomValue();
                if (this.value) {
                    domValue.innerHTML = this._escapeHTML(this.value);
                }
                break;

            case 'input':
                this._getDomValue(true);
                this._updateDomValue();
                break;

            case 'keydown':
            case 'mousedown':
                this.editor.selection = this.editor.getSelection();
                break;

            case 'click':
                if (event.ctrlKey && this.editor.mode.edit) {
                    if (util.isUrl(this.value)) {
                        window.open(this.value, '_blank');
                    }
                }
                break;

            case 'keyup':
                this._getDomValue(true);
                this._updateDomValue();
                break;

            case 'cut':
            case 'paste':
                setTimeout(function () {
                    node._getDomValue(true);
                    node._updateDomValue();
                }, 1);
                break;
        }
    }

    // field events
    var domField = dom.field;
    if (target == domField) {
        switch (type) {
            case 'focus':
                focusNode = this;
                break;

            case 'blur':
            case 'change':
                this._getDomField(true);
                this._updateDomField();
                if (this.field) {
                    domField.innerHTML = this._escapeHTML(this.field);
                }
                break;

            case 'input':
                this._getDomField(true);
                this._updateDomField();
                break;

            case 'keydown':
            case 'mousedown':
                this.editor.selection = this.editor.getSelection();
                break;

            case 'keyup':
                this._getDomField(true);
                this._updateDomField();
                break;

            case 'cut':
            case 'paste':
                setTimeout(function () {
                    node._getDomField(true);
                    node._updateDomField();
                }, 1);
                break;
        }
    }

    // focus
    // when clicked in whitespace left or right from the field or value, set focus
    var domTree = dom.tree;
    if (target == domTree.parentNode) {
        switch (type) {
            case 'click':
                var left = (event.offsetX != undefined) ?
                    (event.offsetX < (this.getLevel() + 1) * 24) :
                    (util.getMouseX(event) < util.getAbsoluteLeft(dom.tdSeparator));// for FF
                if (left || expandable) {
                    // node is expandable when it is an object or array
                    if (domField) {
                        util.setEndOfContentEditable(domField);
                        domField.focus();
                    }
                }
                else {
                    if (domValue) {
                        util.setEndOfContentEditable(domValue);
                        domValue.focus();
                    }
                }
                break;
        }
    }
    if ((target == dom.tdExpand && !expandable) || target == dom.tdField ||
        target == dom.tdSeparator) {
        switch (type) {
            case 'click':
                if (domField) {
                    util.setEndOfContentEditable(domField);
                    domField.focus();
                }
                break;
        }
    }

    if (type == 'keydown') {
        this.onKeyDown(event);
    }
};

/**
 * Key down event handler
 * @param {Event} event
 */
Node.prototype.onKeyDown = function (event) {
    var keynum = event.which || event.keyCode;
    var target = event.target || event.srcElement;
    var ctrlKey = event.ctrlKey;
    var shiftKey = event.shiftKey;
    var altKey = event.altKey;
    var handled = false;
    var prevNode, nextNode, nextDom, nextDom2;

    // util.log(ctrlKey, keynum, event.charCode); // TODO: cleanup
    if (keynum == 13) { // Enter
        if (target == this.dom.value) {
            if (!this.editor.mode.edit || event.ctrlKey) {
                if (util.isUrl(this.value)) {
                    window.open(this.value, '_blank');
                    handled = true;
                }
            }
        }
        else if (target == this.dom.expand) {
            var expandable = this._hasChilds();
            if (expandable) {
                var recurse = event.ctrlKey; // with ctrl-key, expand/collapse all
                this._onExpand(recurse);
                target.focus();
                handled = true;
            }
        }
    }
    else if (keynum == 68) {  // D
        if (ctrlKey) {   // Ctrl+D
            this._onDuplicate();
            handled = true;
        }
    }
    else if (keynum == 69) { // E
        if (ctrlKey) {       // Ctrl+E and Ctrl+Shift+E
            this._onExpand(shiftKey);  // recurse = shiftKey
            target.focus(); // TODO: should restore focus in case of recursing expand (which takes DOM offline)
            handled = true;
        }
    }
    else if (keynum == 77) { // M
        if (ctrlKey) { // Ctrl+M
            this.showContextMenu(target);
            handled = true;
        }
    }
    else if (keynum == 46) { // Del
        if (ctrlKey) {       // Ctrl+Del
            this._onRemove();
            handled = true;
        }
    }
    else if (keynum == 45) { // Ins
        if (ctrlKey && !shiftKey) {       // Ctrl+Ins
            this._onInsertBefore();
            handled = true;
        }
        else if (ctrlKey && shiftKey) {   // Ctrl+Shift+Ins
            this._onInsertAfter();
            handled = true;
        }
    }
    else if (keynum == 35) { // End
        if (altKey) { // Alt+End
            // find the last node
            var lastNode = this._lastNode();
            if (lastNode) {
                lastNode.focus(Node.focusElement || this._getElementName(target));
            }
            handled = true;
        }
    }
    else if (keynum == 36) { // Home
        if (altKey) { // Alt+Home
            // find the first node
            var firstNode = this._firstNode();
            if (firstNode) {
                firstNode.focus(Node.focusElement || this._getElementName(target));
            }
            handled = true;
        }
    }
    else if (keynum == 37) {        // Arrow Left
        if (altKey && !shiftKey) {  // Alt + Arrow Left
            // move to left element
            var prevElement = this._previousElement(target);
            if (prevElement) {
                this.focus(this._getElementName(prevElement));
            }
            handled = true;
        }
        else if (altKey && shiftKey) { // Alt + Shift Arrow left
            if (this.expanded) {
                var appendDom = this.getAppend();
                nextDom = appendDom ? appendDom.nextSibling : undefined;
            }
            else {
                var dom = this.getDom();
                nextDom = dom.nextSibling;
            }
            if (nextDom) {
                nextNode = Node.getNodeFromTarget(nextDom);
                nextDom2 = nextDom.nextSibling;
                nextNode2 = Node.getNodeFromTarget(nextDom2);
                if (nextNode && nextNode instanceof AppendNode &&
                        !(this.parent.childs.length == 1) &&
                        nextNode2 && nextNode2.parent) {
                    nextNode2.parent.moveBefore(this, nextNode2);
                    this.focus(Node.focusElement || this._getElementName(target));
                }
            }
        }
    }
    else if (keynum == 38) {        // Arrow Up
        if (altKey && !shiftKey) {  // Alt + Arrow Up
            // find the previous node
            prevNode = this._previousNode();
            if (prevNode) {
                prevNode.focus(Node.focusElement || this._getElementName(target));
            }
            handled = true;
        }
        else if (altKey && shiftKey) { // Alt + Shift + Arrow Up
            // find the previous node
            prevNode = this._previousNode();
            if (prevNode && prevNode.parent) {
                prevNode.parent.moveBefore(this, prevNode);
                this.focus(Node.focusElement || this._getElementName(target));
            }
            handled = true;
        }
    }
    else if (keynum == 39) {        // Arrow Right
        if (altKey && !shiftKey) {  // Alt + Arrow Right
            // move to right element
            var nextElement = this._nextElement(target);
            if (nextElement) {
                this.focus(this._getElementName(nextElement));
            }
            handled = true;
        }
        else if (altKey && shiftKey) { // Alt + Shift Arrow Right
            dom = this.getDom();
            var prevDom = dom.previousSibling;
            if (prevDom) {
                prevNode = Node.getNodeFromTarget(prevDom);
                if (prevNode && prevNode.parent &&
                        (prevNode instanceof AppendNode)
                        && !prevNode.isVisible()) {
                    prevNode.parent.moveBefore(this, prevNode);
                    this.focus(Node.focusElement || this._getElementName(target));
                }
            }
        }
    }
    else if (keynum == 40) {        // Arrow Down
        if (altKey && !shiftKey) {  // Alt + Arrow Down
            // find the next node
            nextNode = this._nextNode();
            if (nextNode) {
                nextNode.focus(Node.focusElement || this._getElementName(target));
            }
            handled = true;
        }
        else if (altKey && shiftKey) { // Alt + Shift + Arrow Down
            // find the 2nd next node and move before that one
            if (this.expanded) {
                nextNode = this.append ? this.append._nextNode() : undefined;
            }
            else {
                nextNode = this._nextNode();
            }
            nextDom = nextNode ? nextNode.getDom() : undefined;
            if (this.parent.childs.length == 1) {
                nextDom2 = nextDom;
            }
            else {
                nextDom2 = nextDom ? nextDom.nextSibling : undefined;
            }
            var nextNode2 = Node.getNodeFromTarget(nextDom2);
            if (nextNode2 && nextNode2.parent) {
                nextNode2.parent.moveBefore(this, nextNode2);
                this.focus(Node.focusElement || this._getElementName(target));
            }
            handled = true;
        }
    }

    if (handled) {
        util.preventDefault(event);
        util.stopPropagation(event);
    }
};

/**
 * Handle the expand event, when clicked on the expand button
 * @param {boolean} recurse   If true, child nodes will be expanded too
 * @private
 */
Node.prototype._onExpand = function (recurse) {
    if (recurse) {
        // Take the table offline
        var table = this.dom.tr.parentNode; // TODO: not nice to access the main table like this
        var frame = table.parentNode;
        var scrollTop = frame.scrollTop;
        frame.removeChild(table);
    }

    if (this.expanded) {
        this.collapse(recurse);
    }
    else {
        this.expand(recurse);
    }

    if (recurse) {
        // Put the table online again
        frame.appendChild(table);
        frame.scrollTop = scrollTop;
    }
};

/**
 * Remove this node
 * @private
 */
Node.prototype._onRemove = function() {
    this.editor.highlighter.unhighlight();
    var childs = this.parent.childs;
    var index = childs.indexOf(this);

    // adjust the focus
    var oldSelection = this.editor.getSelection();
    if (childs[index + 1]) {
        childs[index + 1].focus();
    }
    else if (childs[index - 1]) {
        childs[index - 1].focus();
    }
    else {
        this.parent.focus();
    }
    var newSelection = this.editor.getSelection();

    // remove the node
    this.parent._remove(this);

    // store history action
    this.editor._onAction('removeNode', {
        'node': this,
        'parent': this.parent,
        'index': index,
        'oldSelection': oldSelection,
        'newSelection': newSelection
    });
};

/**
 * Duplicate this node
 * @private
 */
Node.prototype._onDuplicate = function() {
    var oldSelection = this.editor.getSelection();
    var clone = this.parent._duplicate(this);
    clone.focus();
    var newSelection = this.editor.getSelection();

    this.editor._onAction('duplicateNode', {
        'node': this,
        'clone': clone,
        'parent': this.parent,
        'oldSelection': oldSelection,
        'newSelection': newSelection
    });
};

/**
 * Handle insert before event
 * @param {String} [field]
 * @param {*} [value]
 * @param {String} [type]   Can be 'auto', 'array', 'object', or 'string'
 * @private
 */
Node.prototype._onInsertBefore = function (field, value, type) {
    var oldSelection = this.editor.getSelection();

    var newNode = new Node(this.editor, {
        'field': (field != undefined) ? field : '',
        'value': (value != undefined) ? value : '',
        'type': type
    });
    newNode.expand(true);
    this.parent.insertBefore(newNode, this);
    this.editor.highlighter.unhighlight();
    newNode.focus('field');
    var newSelection = this.editor.getSelection();

    this.editor._onAction('insertBeforeNode', {
        'node': newNode,
        'beforeNode': this,
        'parent': this.parent,
        'oldSelection': oldSelection,
        'newSelection': newSelection
    });
};

/**
 * Handle insert after event
 * @param {String} [field]
 * @param {*} [value]
 * @param {String} [type]   Can be 'auto', 'array', 'object', or 'string'
 * @private
 */
Node.prototype._onInsertAfter = function (field, value, type) {
    var oldSelection = this.editor.getSelection();

    var newNode = new Node(this.editor, {
        'field': (field != undefined) ? field : '',
        'value': (value != undefined) ? value : '',
        'type': type
    });
    newNode.expand(true);
    this.parent.insertAfter(newNode, this);
    this.editor.highlighter.unhighlight();
    newNode.focus('field');
    var newSelection = this.editor.getSelection();

    this.editor._onAction('insertAfterNode', {
        'node': newNode,
        'afterNode': this,
        'parent': this.parent,
        'oldSelection': oldSelection,
        'newSelection': newSelection
    });
};

/**
 * Handle append event
 * @param {String} [field]
 * @param {*} [value]
 * @param {String} [type]   Can be 'auto', 'array', 'object', or 'string'
 * @private
 */
Node.prototype._onAppend = function (field, value, type) {
    var oldSelection = this.editor.getSelection();

    var newNode = new Node(this.editor, {
        'field': (field != undefined) ? field : '',
        'value': (value != undefined) ? value : '',
        'type': type
    });
    newNode.expand(true);
    this.parent.appendChild(newNode);
    this.editor.highlighter.unhighlight();
    newNode.focus('field');
    var newSelection = this.editor.getSelection();

    this.editor._onAction('appendNode', {
        'node': newNode,
        'parent': this.parent,
        'oldSelection': oldSelection,
        'newSelection': newSelection
    });
};

/**
 * Change the type of the node's value
 * @param {String} newType
 * @private
 */
Node.prototype._onChangeType = function (newType) {
    var oldType = this.type;
    if (newType != oldType) {
        var oldSelection = this.editor.getSelection();
        this.changeType(newType);
        var newSelection = this.editor.getSelection();

        this.editor._onAction('changeType', {
            'node': this,
            'oldType': oldType,
            'newType': newType,
            'oldSelection': oldSelection,
            'newSelection': newSelection
        });
    }
};

/**
 * Sort the childs of the node. Only applicable when the node has type 'object'
 * or 'array'.
 * @param {String} direction   Sorting direction. Available values: "asc", "desc"
 * @private
 */
Node.prototype._onSort = function (direction) {
    if (this._hasChilds()) {
        var order = (direction == 'desc') ? -1 : 1;
        var prop = (this.type == 'array') ? 'value': 'field';
        this.hideChilds();

        var oldChilds = this.childs;
        var oldSort = this.sort;

        // copy the array (the old one will be kept for an undo action
        this.childs = this.childs.concat();

        // sort the arrays
        this.childs.sort(function (a, b) {
            if (a[prop] > b[prop]) return order;
            if (a[prop] < b[prop]) return -order;
            return 0;
        });
        this.sort = (order == 1) ? 'asc' : 'desc';

        this.editor._onAction('sort', {
            'node': this,
            'oldChilds': oldChilds,
            'oldSort': oldSort,
            'newChilds': this.childs,
            'newSort': this.sort
        });

        this.showChilds();
    }
};

/**
 * Create a table row with an append button.
 * @return {HTMLElement | undefined} buttonAppend or undefined when inapplicable
 */
Node.prototype.getAppend = function () {
    if (!this.append) {
        this.append = new AppendNode(this.editor);
        this.append.setParent(this);
    }
    return this.append.getDom();
};

/**
 * Find the node from an event target
 * @param {Node} target
 * @return {Node | undefined} node  or undefined when not found
 * @static
 */
Node.getNodeFromTarget = function (target) {
    while (target) {
        if (target.node) {
            return target.node;
        }
        target = target.parentNode;
    }

    return undefined;
};

/**
 * Get the previously rendered node
 * @return {Node | null} previousNode
 * @private
 */
Node.prototype._previousNode = function () {
    var prevNode = null;
    var dom = this.getDom();
    if (dom && dom.parentNode) {
        // find the previous field
        var prevDom = dom;
        do {
            prevDom = prevDom.previousSibling;
            prevNode = Node.getNodeFromTarget(prevDom);
        }
        while (prevDom && (prevNode instanceof AppendNode && !prevNode.isVisible()));
    }
    return prevNode;
};

/**
 * Get the next rendered node
 * @return {Node | null} nextNode
 * @private
 */
Node.prototype._nextNode = function () {
    var nextNode = null;
    var dom = this.getDom();
    if (dom && dom.parentNode) {
        // find the previous field
        var nextDom = dom;
        do {
            nextDom = nextDom.nextSibling;
            nextNode = Node.getNodeFromTarget(nextDom);
        }
        while (nextDom && (nextNode instanceof AppendNode && !nextNode.isVisible()));
    }

    return nextNode;
};

/**
 * Get the first rendered node
 * @return {Node | null} firstNode
 * @private
 */
Node.prototype._firstNode = function () {
    var firstNode = null;
    var dom = this.getDom();
    if (dom && dom.parentNode) {
        var firstDom = dom.parentNode.firstChild;
        firstNode = Node.getNodeFromTarget(firstDom);
    }

    return firstNode;
};

/**
 * Get the last rendered node
 * @return {Node | null} lastNode
 * @private
 */
Node.prototype._lastNode = function () {
    var lastNode = null;
    var dom = this.getDom();
    if (dom && dom.parentNode) {
        var lastDom = dom.parentNode.lastChild;
        lastNode =  Node.getNodeFromTarget(lastDom);
        while (lastDom && (lastNode instanceof AppendNode && !lastNode.isVisible())) {
            lastDom = lastDom.previousSibling;
            lastNode =  Node.getNodeFromTarget(lastDom);
        }
    }
    return lastNode;
};

/**
 * Get the next element which can have focus.
 * @param {Element} elem
 * @return {Element | null} nextElem
 * @private
 */
Node.prototype._previousElement = function (elem) {
    var dom = this.dom;
    // noinspection FallthroughInSwitchStatementJS
    switch (elem) {
        case dom.value:
            if (this.fieldEditable) {
                return dom.field;
            }
        // intentional fall through
        case dom.field:
            if (this._hasChilds()) {
                return dom.expand;
            }
        // intentional fall through
        case dom.expand:
            return dom.menu;
        case dom.menu:
            if (dom.drag) {
                return dom.drag;
            }
        // intentional fall through
        default:
            return null;
    }
};

/**
 * Get the next element which can have focus.
 * @param {Element} elem
 * @return {Element | null} nextElem
 * @private
 */
Node.prototype._nextElement = function (elem) {
    var dom = this.dom;
    // noinspection FallthroughInSwitchStatementJS
    switch (elem) {
        case dom.drag:
            return dom.menu;
        case dom.menu:
            if (this._hasChilds()) {
                return dom.expand;
            }
        // intentional fall through
        case dom.expand:
            if (this.fieldEditable) {
                return dom.field;
            }
        // intentional fall through
        case dom.field:
            if (!this._hasChilds()) {
                return dom.value;
            }
        default:
            return null;
    }
};

/**
 * Get the dom name of given element. returns null if not found.
 * For example when element == dom.field, "field" is returned.
 * @param {Element} element
 * @return {String | null} elementName  Available elements with name: 'drag',
 *                                      'menu', 'expand', 'field', 'value'
 * @private
 */
Node.prototype._getElementName = function (element) {
    var dom = this.dom;
    for (var name in dom) {
        if (dom.hasOwnProperty(name)) {
            if (dom[name] == element) {
                return name;
            }
        }
    }
    return null;
};

/**
 * Test if this node has childs. This is the case when the node is an object
 * or array.
 * @return {boolean} hasChilds
 * @private
 */
Node.prototype._hasChilds = function () {
    return this.type == 'array' || this.type == 'object';
};

// titles with explanation for the different types
Node.TYPE_TITLES = {
    'auto': 'Field type "自动". ' +
        'The field type is automatically determined from the value ' +
        'and can be a string, number, boolean, or null.',
    'object': 'Field type "对象". ' +
        'An object contains an unordered set of key/value pairs.',
    'array': 'Field type "数组". ' +
        'An array contains an ordered collection of values.',
    'string': 'Field type "字符串". ' +
        'Field type is not determined from the value, ' +
        'but always returned as string.'
};

/**
 * Show a contextmenu for this node
 * @param {HTMLElement} anchor   Anchor element to attache the context menu to.
 * @param {function} [onClose]   Callback method called when the context menu
 *                               is being closed.
 */
Node.prototype.showContextMenu = function (anchor, onClose) {
    var node = this;
    var titles = Node.TYPE_TITLES;
    var items = [];

    items.push({
        'text': 'Type',
        'title': 'Change the type of this field',
        'className': 'type-' + this.type,
        'submenu': [
            {
                'text': 'Auto',
                'className': 'type-auto' +
                    (this.type == 'auto' ? ' selected' : ''),
                'title': titles.auto,
                'click': function () {
                    node._onChangeType('auto');
                }
            },
            {
                'text': 'Array',
                'className': 'type-array' +
                    (this.type == 'array' ? ' selected' : ''),
                'title': titles.array,
                'click': function () {
                    node._onChangeType('array');
                }
            },
            {
                'text': 'Object',
                'className': 'type-object' +
                    (this.type == 'object' ? ' selected' : ''),
                'title': titles.object,
                'click': function () {
                    node._onChangeType('object');
                }
            },
            {
                'text': 'String',
                'className': 'type-string' +
                    (this.type == 'string' ? ' selected' : ''),
                'title': titles.string,
                'click': function () {
                    node._onChangeType('string');
                }
            }
        ]
    });

    if (this._hasChilds()) {
        var direction = ((this.sort == 'asc') ? 'desc': 'asc');
        items.push({
            'text': 'Sort',
            'title': 'Sort the childs of this ' + this.type,
            'className': 'sort-' + direction,
            'click': function () {
                node._onSort(direction);
            },
            'submenu': [
                {
                    'text': 'Ascending',
                    'className': 'sort-asc',
                    'title': 'Sort the childs of this ' + this.type + ' in ascending order',
                    'click': function () {
                        node._onSort('asc');
                    }
                },
                {
                    'text': 'Descending',
                    'className': 'sort-desc',
                    'title': 'Sort the childs of this ' + this.type +' in descending order',
                    'click': function () {
                        node._onSort('desc');
                    }
                }
            ]
        });
    }

    if (this.parent && this.parent._hasChilds()) {
        // create a separator
        items.push({
            'type': 'separator'
        });

        // create append button (for last child node only)
        var childs = node.parent.childs;
        if (node == childs[childs.length - 1]) {
            items.push({
                'text': 'Append',
                'title': 'Append a new field with type \'auto\' after this field (Ctrl+Shift+Ins)',
                'submenuTitle': 'Select the type of the field to be appended',
                'className': 'append',
                'click': function () {
                    node._onAppend('', '', 'auto');
                },
                'submenu': [
                    {
                        'text': 'Auto',
                        'className': 'type-auto',
                        'title': titles.auto,
                        'click': function () {
                            node._onAppend('', '', 'auto');
                        }
                    },
                    {
                        'text': 'Array',
                        'className': 'type-array',
                        'title': titles.array,
                        'click': function () {
                            node._onAppend('', []);
                        }
                    },
                    {
                        'text': 'Object',
                        'className': 'type-object',
                        'title': titles.object,
                        'click': function () {
                            node._onAppend('', {});
                        }
                    },
                    {
                        'text': 'String',
                        'className': 'type-string',
                        'title': titles.string,
                        'click': function () {
                            node._onAppend('', '', 'string');
                        }
                    }
                ]
            });
        }

        // create insert button
        items.push({
            'text': 'Insert',
            'title': 'Insert a new field with type \'auto\' before this field (Ctrl+Ins)',
            'submenuTitle': 'Select the type of the field to be inserted',
            'className': 'insert',
            'click': function () {
                node._onInsertBefore('', '', 'auto');
            },
            'submenu': [
                {
                    'text': 'Auto',
                    'className': 'type-auto',
                    'title': titles.auto,
                    'click': function () {
                        node._onInsertBefore('', '', 'auto');
                    }
                },
                {
                    'text': 'Array',
                    'className': 'type-array',
                    'title': titles.array,
                    'click': function () {
                        node._onInsertBefore('', []);
                    }
                },
                {
                    'text': 'Object',
                    'className': 'type-object',
                    'title': titles.object,
                    'click': function () {
                        node._onInsertBefore('', {});
                    }
                },
                {
                    'text': 'String',
                    'className': 'type-string',
                    'title': titles.string,
                    'click': function () {
                        node._onInsertBefore('', '', 'string');
                    }
                }
            ]
        });

        // create duplicate button
        items.push({
            'text': 'Duplicate',
            'title': 'Duplicate this field (Ctrl+D)',
            'className': 'duplicate',
            'click': function () {
                node._onDuplicate();
            }
        });

        // create remove button
        items.push({
            'text': 'Remove',
            'title': 'Remove this field (Ctrl+Del)',
            'className': 'remove',
            'click': function () {
                node._onRemove();
            }
        });
    }

    var menu = new ContextMenu(items, {close: onClose});
    menu.show(anchor);
};

/**
 * get the type of a value
 * @param {*} value
 * @return {String} type   Can be 'object', 'array', 'string', 'auto'
 * @private
 */
Node.prototype._getType = function(value) {
    if (value instanceof Array) {
        return 'array';
    }
    if (value instanceof Object) {
        return 'object';
    }
    if (typeof(value) == 'string' && typeof(this._stringCast(value)) != 'string') {
        return 'string';
    }

    return 'auto';
};

/**
 * cast contents of a string to the correct type. This can be a string,
 * a number, a boolean, etc
 * @param {String} str
 * @return {*} castedStr
 * @private
 */
Node.prototype._stringCast = function(str) {
    var lower = str.toLowerCase(),
        num = Number(str),          // will nicely fail with '123ab'
        numFloat = parseFloat(str); // will nicely fail with '  '

    if (str == '') {
        return '';
    }
    else if (lower == 'null') {
        return null;
    }
    else if (lower == 'true') {
        return true;
    }
    else if (lower == 'false') {
        return false;
    }
    else if (!isNaN(num) && !isNaN(numFloat)) {
        return num;
    }
    else {
        return str;
    }
};

/**
 * escape a text, such that it can be displayed safely in an HTML element
 * @param {String} text
 * @return {String} escapedText
 * @private
 */
Node.prototype._escapeHTML = function (text) {
    var htmlEscaped = String(text)
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/  /g, ' &nbsp;') // replace double space with an nbsp and space
        .replace(/^ /, '&nbsp;')   // space at start
        .replace(/ $/, '&nbsp;');  // space at end

    var json = JSON.stringify(htmlEscaped);
    return json.substring(1, json.length - 1);
};

/**
 * unescape a string.
 * @param {String} escapedText
 * @return {String} text
 * @private
 */
Node.prototype._unescapeHTML = function (escapedText) {
    var json = '"' + this._escapeJSON(escapedText) + '"';
    var htmlEscaped = util.parse(json);
    return htmlEscaped
        .replace(/&lt;/g, '<')
        .replace(/&gt;/g, '>')
        .replace(/&nbsp;/g, ' ');
};

/**
 * escape a text to make it a valid JSON string. The method will:
 *   - replace unescaped double quotes with '\"'
 *   - replace unescaped backslash with '\\'
 *   - replace returns with '\n'
 * @param {String} text
 * @return {String} escapedText
 * @private
 */
Node.prototype._escapeJSON = function (text) {
    // TODO: replace with some smart regex (only when a new solution is faster!)
    var escaped = '';
    var i = 0, iMax = text.length;
    while (i < iMax) {
        var c = text.charAt(i);
        if (c == '\n') {
            escaped += '\\n';
        }
        else if (c == '\\') {
            escaped += c;
            i++;

            c = text.charAt(i);
            if ('"\\/bfnrtu'.indexOf(c) == -1) {
                escaped += '\\';  // no valid escape character
            }
            escaped += c;
        }
        else if (c == '"') {
            escaped += '\\"';
        }
        else {
            escaped += c;
        }
        i++;
    }

    return escaped;
};

/**
 * @constructor AppendNode
 * @extends Node
 * @param {TreeEditor} editor
 * Create a new AppendNode. This is a special node which is created at the
 * end of the list with childs for an object or array
 */
function AppendNode (editor) {
    /** @type {TreeEditor} */
    this.editor = editor;
    this.dom = {};
}

AppendNode.prototype = new Node();

/**
 * Return a table row with an append button.
 * @return {Element} dom   TR element
 */
AppendNode.prototype.getDom = function () {
    // TODO: implement a new solution for the append node
    var dom = this.dom;

    if (dom.tr) {
        return dom.tr;
    }

    // a row for the append button
    var trAppend = document.createElement('tr');
    trAppend.node = this;
    dom.tr = trAppend;

    // TODO: consistent naming

    if (this.editor.mode.edit) {
        // a cell for the dragarea column
        dom.tdDrag = document.createElement('td');

        // create context menu
        var tdMenu = document.createElement('td');
        dom.tdMenu = tdMenu;
        var menu = document.createElement('button');
        menu.className = 'contextmenu';
        menu.title = 'Click to open the actions menu (Ctrl+M)';
        dom.menu = menu;
        tdMenu.appendChild(dom.menu);
    }

    // a cell for the contents (showing text 'empty')
    var tdAppend = document.createElement('td');
    var domText = document.createElement('div');
    domText.innerHTML = '(empty)';
    domText.className = 'readonly';
    tdAppend.appendChild(domText);
    dom.td = tdAppend;
    dom.text = domText;

    this.updateDom();

    return trAppend;
};

/**
 * Update the HTML dom of the Node
 */
AppendNode.prototype.updateDom = function () {
    var dom = this.dom;
    var tdAppend = dom.td;
    if (tdAppend) {
        tdAppend.style.paddingLeft = (this.getLevel() * 24 + 26) + 'px';
        // TODO: not so nice hard coded offset
    }

    var domText = dom.text;
    if (domText) {
        domText.innerHTML = '(empty ' + this.parent.type + ')';
    }

    // attach or detach the contents of the append node:
    // hide when the parent has childs, show when the parent has no childs
    var trAppend = dom.tr;
    if (!this.isVisible()) {
        if (dom.tr.firstChild) {
            if (dom.tdDrag) {
                trAppend.removeChild(dom.tdDrag);
            }
            if (dom.tdMenu) {
                trAppend.removeChild(dom.tdMenu);
            }
            trAppend.removeChild(tdAppend);
        }
    }
    else {
        if (!dom.tr.firstChild) {
            if (dom.tdDrag) {
                trAppend.appendChild(dom.tdDrag);
            }
            if (dom.tdMenu) {
                trAppend.appendChild(dom.tdMenu);
            }
            trAppend.appendChild(tdAppend);
        }
    }
};

/**
 * Check whether the AppendNode is currently visible.
 * the AppendNode is visible when its parent has no childs (i.e. is empty).
 * @return {boolean} isVisible
 */
AppendNode.prototype.isVisible = function () {
    return (this.parent.childs.length == 0);
};

/**
 * Show a contextmenu for this node
 * @param {HTMLElement} anchor   The element to attach the menu to.
 * @param {function} [onClose]   Callback method called when the context menu
 *                               is being closed.
 */
AppendNode.prototype.showContextMenu = function (anchor, onClose) {
    var node = this;
    var titles = Node.TYPE_TITLES;
    var items = [
        // create append button
        {
            'text': 'Append',
            'title': 'Append a new field with type \'auto\' (Ctrl+Shift+Ins)',
            'submenuTitle': 'Select the type of the field to be appended',
            'className': 'insert',
            'click': function () {
                node._onAppend('', '', 'auto');
            },
            'submenu': [
                {
                    'text': 'Auto',
                    'className': 'type-auto',
                    'title': titles.auto,
                    'click': function () {
                        node._onAppend('', '', 'auto');
                    }
                },
                {
                    'text': 'Array',
                    'className': 'type-array',
                    'title': titles.array,
                    'click': function () {
                        node._onAppend('', []);
                    }
                },
                {
                    'text': 'Object',
                    'className': 'type-object',
                    'title': titles.object,
                    'click': function () {
                        node._onAppend('', {});
                    }
                },
                {
                    'text': 'String',
                    'className': 'type-string',
                    'title': titles.string,
                    'click': function () {
                        node._onAppend('', '', 'string');
                    }
                }
            ]
        }
    ];

    var menu = new ContextMenu(items, {close: onClose});
    menu.show(anchor);
};

/**
 * Handle an event. The event is catched centrally by the editor
 * @param {Event} event
 */
AppendNode.prototype.onEvent = function (event) {
    var type = event.type;
    var target = event.target || event.srcElement;
    var dom = this.dom;

    // highlight the append nodes parent
    var menu = dom.menu;
    if (target == menu) {
        if (type == 'mouseover') {
            this.editor.highlighter.highlight(this.parent);
        }
        else if (type == 'mouseout') {
            this.editor.highlighter.unhighlight();
        }
    }

    // context menu events
    if (type == 'click' && target == dom.menu) {
        var highlighter = this.editor.highlighter;
        highlighter.highlight(this.parent);
        highlighter.lock();
        util.addClassName(dom.menu, 'selected');
        this.showContextMenu(dom.menu, function () {
            util.removeClassName(dom.menu, 'selected');
            highlighter.unlock();
            highlighter.unhighlight();
        });
    }

    if (type == 'keydown') {
        this.onKeyDown(event);
    }
};

/**
 * A context menu
 * @param {Object[]} items    Array containing the menu structure
 *                            TODO: describe structure
 * @param {Object} [options]  Object with options. Available options:
 *                            {function} close    Callback called when the
 *                                                context menu is being closed.
 * @constructor
 */
function ContextMenu (items, options) {
    this.dom = {};

    var me = this;
    var dom = this.dom;
    this.anchor = undefined;
    this.items = items;
    this.eventListeners = {};
    this.selection = undefined; // holds the selection before the menu was opened
    this.visibleSubmenu = undefined;
    this.onClose = options ? options.close : undefined;

    // create a container element
    var menu = document.createElement('div');
    menu.className = 'jsoneditor-contextmenu';
    dom.menu = menu;

    // create a list to hold the menu items
    var list = document.createElement('ul');
    list.className = 'menu';
    menu.appendChild(list);
    dom.list = list;
    dom.items = []; // list with all buttons

    // create a (non-visible) button to set the focus to the menu
    var focusButton = document.createElement('button');
    dom.focusButton = focusButton;
    var li = document.createElement('li');
    li.style.overflow = 'hidden';
    li.style.height = '0';
    li.appendChild(focusButton);
    list.appendChild(li);

    function createMenuItems (list, domItems, items) {
        items.forEach(function (item) {
            if (item.type == 'separator') {
                // create a separator
                var separator = document.createElement('div');
                separator.className = 'separator';
                li = document.createElement('li');
                li.appendChild(separator);
                list.appendChild(li);
            }
            else {
                var domItem = {};

                // create a menu item
                var li = document.createElement('li');
                list.appendChild(li);

                // create a button in the menu item
                var button = document.createElement('button');
                button.className = item.className;
                domItem.button = button;
                if (item.title) {
                    button.title = item.title;
                }
                if (item.click) {
                    button.onclick = function () {
                        me.hide();
                        item.click();
                    };
                }
                li.appendChild(button);

                // create the contents of the button
                if (item.submenu) {
                    // add the icon to the button
                    var divIcon = document.createElement('div');
                    divIcon.className = 'icon';
                    button.appendChild(divIcon);
                    button.appendChild(document.createTextNode(item.text));

                    var buttonSubmenu;
                    if (item.click) {
                        // submenu and a button with a click handler
                        button.className += ' default';

                        var buttonExpand = document.createElement('button');
                        domItem.buttonExpand = buttonExpand;
                        buttonExpand.className = 'expand';
                        buttonExpand.innerHTML = '<div class="expand"></div>';
                        li.appendChild(buttonExpand);
                        if (item.submenuTitle) {
                            buttonExpand.title = item.submenuTitle;
                        }

                        buttonSubmenu = buttonExpand;
                    }
                    else {
                        // submenu and a button without a click handler
                        var divExpand = document.createElement('div');
                        divExpand.className = 'expand';
                        button.appendChild(divExpand);

                        buttonSubmenu = button;
                    }

                    // attach a handler to expand/collapse the submenu
                    buttonSubmenu.onclick = function () {
                        me._onExpandItem(domItem);
                        buttonSubmenu.focus();
                    };

                    // create the submenu
                    var domSubItems = [];
                    domItem.subItems = domSubItems;
                    var ul = document.createElement('ul');
                    domItem.ul = ul;
                    ul.className = 'menu';
                    ul.style.height = '0';
                    li.appendChild(ul);
                    createMenuItems(ul, domSubItems, item.submenu);
                }
                else {
                    // no submenu, just a button with clickhandler
                    button.innerHTML = '<div class="icon"></div>' + item.text;
                }

                domItems.push(domItem);
            }
        });
    }
    createMenuItems(list, this.dom.items, items);

    // TODO: when the editor is small, show the submenu on the right instead of inline?

    // calculate the max height of the menu with one submenu expanded
    this.maxHeight = 0; // height in pixels
    items.forEach(function (item) {
        var height = (items.length + (item.submenu ? item.submenu.length : 0)) * 24;
        me.maxHeight = Math.max(me.maxHeight, height);
    });
}

/**
 * Get the currently visible buttons
 * @return {Array.<HTMLElement>} buttons
 * @private
 */
ContextMenu.prototype._getVisibleButtons = function () {
    var buttons = [];
    var me = this;
    this.dom.items.forEach(function (item) {
        buttons.push(item.button);
        if (item.buttonExpand) {
            buttons.push(item.buttonExpand);
        }
        if (item.subItems && item == me.expandedItem) {
            item.subItems.forEach(function (subItem) {
                buttons.push(subItem.button);
                if (subItem.buttonExpand) {
                    buttons.push(subItem.buttonExpand);
                }
                // TODO: change to fully recursive method
            });
        }
    });

    return buttons;
};

// currently displayed context menu, a singleton. We may only have one visible context menu
ContextMenu.visibleMenu = undefined;

/**
 * Attach the menu to an anchor
 * @param {HTMLElement} anchor
 */
ContextMenu.prototype.show = function (anchor) {
    this.hide();

    // calculate whether the menu fits below the anchor
    var windowHeight = util.getWindowHeight();
    var anchorHeight = anchor.offsetHeight;
    var menuHeight = this.maxHeight;

    // position the menu
    var left = util.getAbsoluteLeft(anchor);
    var top = util.getAbsoluteTop(anchor);
    if (top + anchorHeight + menuHeight < windowHeight) {
        // display the menu below the anchor
        this.dom.menu.style.left = left + 'px';
        this.dom.menu.style.top = (top + anchorHeight - scrollHeight()) + 'px';
        this.dom.menu.style.bottom = '';
    }
    else {
        // display the menu above the anchor
        this.dom.menu.style.left = left + 'px';
        this.dom.menu.style.top = '';
        this.dom.menu.style.bottom = (windowHeight - top + scrollHeight()) + 'px';
    }

    // attach the menu to the document
    document.body.appendChild(this.dom.menu);

    // create and attach event listeners
    var me = this;
    var list = this.dom.list;
    this.eventListeners.mousedown = util.addEventListener(
        document, 'mousedown', function (event) {
            // hide menu on click outside of the menu
            event = event || window.event;
            var target = event.target || event.srcElement;
            if ((target != list) && !me._isChildOf(target, list)) {
                me.hide();
                util.stopPropagation(event);
                util.preventDefault(event);
            }
        });
    this.eventListeners.mousewheel = util.addEventListener(
        document, 'mousewheel', function () {
            // hide the menu on mouse scroll
            util.stopPropagation(event);
            util.preventDefault(event);
        });
    this.eventListeners.keydown = util.addEventListener(
        document, 'keydown', function (event) {
            me._onKeyDown(event);
        });

    // move focus to the first button in the context menu
    this.selection = util.getSelection();
    this.anchor = anchor;
    setTimeout(function () {
        me.dom.focusButton.focus();
    }, 0);

    if (ContextMenu.visibleMenu) {
        ContextMenu.visibleMenu.hide();
    }
    ContextMenu.visibleMenu = this;
};

/**
 * Hide the context menu if visible
 */
ContextMenu.prototype.hide = function () {
    // remove the menu from the DOM
    if (this.dom.menu.parentNode) {
        this.dom.menu.parentNode.removeChild(this.dom.menu);
        if (this.onClose) {
            this.onClose();
        }
    }

    // remove all event listeners
    // all event listeners are supposed to be attached to document.
    for (var name in this.eventListeners) {
        if (this.eventListeners.hasOwnProperty(name)) {
            var fn = this.eventListeners[name];
            if (fn) {
                util.removeEventListener(document, name, fn);
            }
            delete this.eventListeners[name];
        }
    }

    if (ContextMenu.visibleMenu == this) {
        ContextMenu.visibleMenu = undefined;
    }
};

/**
 * Expand a submenu
 * Any currently expanded submenu will be hided.
 * @param {Object} domItem
 * @private
 */
ContextMenu.prototype._onExpandItem = function (domItem) {
    var me = this;
    var alreadyVisible = (domItem == this.expandedItem);

    // hide the currently visible submenu
    var expandedItem = this.expandedItem;
    if (expandedItem) {
        //var ul = expandedItem.ul;
        expandedItem.ul.style.height = '0';
        expandedItem.ul.style.padding = '';
        setTimeout(function () {
            if (me.expandedItem != expandedItem) {
                expandedItem.ul.style.display = '';
                util.removeClassName(expandedItem.ul.parentNode, 'selected');
            }
        }, 300); // timeout duration must match the css transition duration
        this.expandedItem = undefined;
    }

    if (!alreadyVisible) {
        var ul = domItem.ul;
        ul.style.display = 'block';
        var height = ul.clientHeight; // force a reflow in Firefox
        setTimeout(function () {
            if (me.expandedItem == domItem) {
                ul.style.height = (ul.childNodes.length * 24) + 'px';
                ul.style.padding = '5px 10px';
            }
        }, 0);
        util.addClassName(ul.parentNode, 'selected');
        this.expandedItem = domItem;
    }
};
/**
 * Handle onkeydown event
 * @param {Event} event
 * @private
 */
ContextMenu.prototype._onKeyDown = function (event) {
    event = event || window.event;
    var target = event.target || event.srcElement;
    var keynum = event.which || event.keyCode;
    var handled = false;
    var buttons, targetIndex, prevButton, nextButton;

    if (keynum == 27) { // ESC
        // hide the menu on ESC key

        // restore previous selection and focus
        if (this.selection) {
            util.setSelection(this.selection);
        }
        if (this.anchor) {
            this.anchor.focus();
        }

        this.hide();

        handled = true;
    }
    else if (keynum == 9) { // Tab
        if (!event.shiftKey) { // Tab
            buttons = this._getVisibleButtons();
            targetIndex = buttons.indexOf(target);
            if (targetIndex == buttons.length - 1) {
                // move to first button
                buttons[0].focus();
                handled = true;
            }
        }
        else { // Shift+Tab
            buttons = this._getVisibleButtons();
            targetIndex = buttons.indexOf(target);
            if (targetIndex == 0) {
                // move to last button
                buttons[buttons.length - 1].focus();
                handled = true;
            }
        }
    }
    else if (keynum == 37) { // Arrow Left
        if (target.className == 'expand') {
            buttons = this._getVisibleButtons();
            targetIndex = buttons.indexOf(target);
            prevButton = buttons[targetIndex - 1];
            if (prevButton) {
                prevButton.focus();
            }
        }
        handled = true;
    }
    else if (keynum == 38) { // Arrow Up
        buttons = this._getVisibleButtons();
        targetIndex = buttons.indexOf(target);
        prevButton = buttons[targetIndex - 1];
        if (prevButton && prevButton.className == 'expand') {
            // skip expand button
            prevButton = buttons[targetIndex - 2];
        }
        if (!prevButton) {
            // move to last button
            prevButton = buttons[buttons.length - 1];
        }
        if (prevButton) {
            prevButton.focus();
        }
        handled = true;
    }
    else if (keynum == 39) { // Arrow Right
        buttons = this._getVisibleButtons();
        targetIndex = buttons.indexOf(target);
        nextButton = buttons[targetIndex + 1];
        if (nextButton && nextButton.className == 'expand') {
            nextButton.focus();
        }
        handled = true;
    }
    else if (keynum == 40) { // Arrow Down
        buttons = this._getVisibleButtons();
        targetIndex = buttons.indexOf(target);
        nextButton = buttons[targetIndex + 1];
        if (nextButton && nextButton.className == 'expand') {
            // skip expand button
            nextButton = buttons[targetIndex + 2];
        }
        if (!nextButton) {
            // move to first button
            nextButton = buttons[0];
        }
        if (nextButton) {
            nextButton.focus();
            handled = true;
        }
        handled = true;
    }
    // TODO: arrow left and right

    if (handled) {
        util.stopPropagation(event);
        util.preventDefault(event);
    }
};

/**
 * Test if an element is a child of a parent element.
 * @param {Element} child
 * @param {Element} parent
 * @return {boolean} isChild
 */
ContextMenu.prototype._isChildOf = function (child, parent) {
    var e = child.parentNode;
    while (e) {
        if (e == parent) {
            return true;
        }
        e = e.parentNode;
    }

    return false;
};

function scrollHeight(){
	var scrollTop=0;
	if(document.documentElement&&document.documentElement.scrollTop){
		scrollTop=document.documentElement.scrollTop;
	}
	else if(document.body){
		scrollTop=document.body.scrollTop;
	}
	return scrollTop;
}
/**
 * @constructor History
 * Store action history, enables undo and redo
 * @param {JSONEditor} editor
 */
function History (editor) {
    this.editor = editor;
    this.clear();

    // map with all supported actions
    this.actions = {
        'editField': {
            'undo': function (params) {
                params.node.updateField(params.oldValue);
            },
            'redo': function (params) {
                params.node.updateField(params.newValue);
            }
        },
        'editValue': {
            'undo': function (params) {
                params.node.updateValue(params.oldValue);
            },
            'redo': function (params) {
                params.node.updateValue(params.newValue);
            }
        },
        'appendNode': {
            'undo': function (params) {
                params.parent.removeChild(params.node);
            },
            'redo': function (params) {
                params.parent.appendChild(params.node);
            }
        },
        'insertBeforeNode': {
            'undo': function (params) {
                params.parent.removeChild(params.node);
            },
            'redo': function (params) {
                params.parent.insertBefore(params.node, params.beforeNode);
            }
        },
        'insertAfterNode': {
            'undo': function (params) {
                params.parent.removeChild(params.node);
            },
            'redo': function (params) {
                params.parent.insertAfter(params.node, params.afterNode);
            }
        },
        'removeNode': {
            'undo': function (params) {
                var parent = params.parent;
                var beforeNode = parent.childs[params.index] || parent.append;
                parent.insertBefore(params.node, beforeNode);
            },
            'redo': function (params) {
                params.parent.removeChild(params.node);
            }
        },
        'duplicateNode': {
            'undo': function (params) {
                params.parent.removeChild(params.clone);
            },
            'redo': function (params) {
                params.parent.insertAfter(params.clone, params.node);
            }
        },
        'changeType': {
            'undo': function (params) {
                params.node.changeType(params.oldType);
            },
            'redo': function (params) {
                params.node.changeType(params.newType);
            }
        },
        'moveNode': {
            'undo': function (params) {
                params.startParent.moveTo(params.node, params.startIndex);
            },
            'redo': function (params) {
                params.endParent.moveTo(params.node, params.endIndex);
            }
        },
        'sort': {
            'undo': function (params) {
                var node = params.node;
                node.hideChilds();
                node.sort = params.oldSort;
                node.childs = params.oldChilds;
                node.showChilds();
            },
            'redo': function (params) {
                var node = params.node;
                node.hideChilds();
                node.sort = params.newSort;
                node.childs = params.newChilds;
                node.showChilds();
            }
        }

        // TODO: restore the original caret position and selection with each undo
        // TODO: implement history for actions "expand", "collapse", "scroll", "setDocument"
    };
}

/**
 * The method onChange is executed when the History is changed, and can
 * be overloaded.
 */
History.prototype.onChange = function () {};

/**
 * Add a new action to the history
 * @param {String} action  The executed action. Available actions: "editField",
 *                         "editValue", "changeType", "appendNode",
 *                         "removeNode", "duplicateNode", "moveNode"
 * @param {Object} params  Object containing parameters describing the change.
 *                         The parameters in params depend on the action (for
 *                         example for "editValue" the Node, old value, and new
 *                         value are provided). params contains all information
 *                         needed to undo or redo the action.
 */
History.prototype.add = function (action, params) {
    this.index++;
    this.history[this.index] = {
        'action': action,
        'params': params,
        'timestamp': new Date()
    };

    // remove redo actions which are invalid now
    if (this.index < this.history.length - 1) {
        this.history.splice(this.index + 1, this.history.length - this.index - 1);
    }

    // fire onchange event
    this.onChange();
};

/**
 * Clear history
 */
History.prototype.clear = function () {
    this.history = [];
    this.index = -1;

    // fire onchange event
    this.onChange();
};

/**
 * Check if there is an action available for undo
 * @return {Boolean} canUndo
 */
History.prototype.canUndo = function () {
    return (this.index >= 0);
};

/**
 * Check if there is an action available for redo
 * @return {Boolean} canRedo
 */
History.prototype.canRedo = function () {
    return (this.index < this.history.length - 1);
};

/**
 * Undo the last action
 */
History.prototype.undo = function () {
    if (this.canUndo()) {
        var obj = this.history[this.index];
        if (obj) {
            var action = this.actions[obj.action];
            if (action && action.undo) {
                action.undo(obj.params);
                if (obj.params.oldSelection) {
                    this.editor.setSelection(obj.params.oldSelection);
                }
            }
            else {
                util.log('Error: unknown action "' + obj.action + '"');
            }
        }
        this.index--;

        // fire onchange event
        this.onChange();
    }
};

/**
 * Redo the last action
 */
History.prototype.redo = function () {
    if (this.canRedo()) {
        this.index++;

        var obj = this.history[this.index];
        if (obj) {
            var action = this.actions[obj.action];
            if (action && action.redo) {
                action.redo(obj.params);
                if (obj.params.newSelection) {
                    this.editor.setSelection(obj.params.newSelection);
                }
            }
            else {
                util.log('Error: unknown action "' + obj.action + '"');
            }
        }

        // fire onchange event
        this.onChange();
    }
};

/**
 * create a mode box to be used in the editor menu's
 * @param {JSONEditor} editor
 * @param {String[]} modes  Available modes: 'code', 'form', 'text', 'tree', 'view'
 * @param {String} current  Available modes: 'code', 'form', 'text', 'tree', 'view'
 * @returns {HTMLElement} box
 */
function createModeBox(editor, modes, current) {
    /**
     * Switch the mode of the editor
     * @param {String} mode
     */
    function switchMode(mode) {
        // switch mode
        editor.setMode(mode);

        // restore focus on mode box
        var modeBox = editor.dom && editor.dom.modeBox;
        if (modeBox) {
            modeBox.focus();
        }
    }

    // available modes
    var availableModes = {
        code: {
            'text': '代码',
            'title': 'Switch to code highlighter',
            'click': function () {
                switchMode('code')
            }
        },
        form: {
            'text': '构成',
            'title': 'Switch to form editor',
            'click': function () {
                switchMode('form');
            }
        },
        text: {
            'text': '文本',
            'title': 'Switch to plain text editor',
            'click': function () {
                switchMode('text');
            }
        },
        tree: {
            'text': '树形',
            'title': 'Switch to tree editor',
            'click': function () {
                switchMode('tree');
            }
        },
        view: {
            'text': '查看',
            'title': 'Switch to tree view',
            'click': function () {
                switchMode('view');
            }
        }
    };

    // list the selected modes
    var items = [];
    for (var i = 0; i < modes.length; i++) {
        var mode = modes[i];
        var item = availableModes[mode];
        if (!item) {
            throw new Error('Unknown mode "' + mode + '"');
        }

        item.className = 'type-modes' + ((current == mode) ? ' selected' : '');
        items.push(item);
    }

    // retrieve the title of current mode
    var currentMode = availableModes[current];
    if (!currentMode) {
        throw new Error('Unknown mode "' + current + '"');
    }
    var currentTitle = currentMode.text;

    // create the html element
    var box = document.createElement('button');
    box.className = 'modes separator';
    box.innerHTML = currentTitle + ' &#x25BE;';
    box.title = 'Switch editor mode';
    box.onclick = function () {
        var menu = new ContextMenu(items);
        menu.show(box);
    };

    return box;
}

/**
 * @constructor SearchBox
 * Create a search box in given HTML container
 * @param {JSONEditor} editor    The JSON Editor to attach to
 * @param {Element} container               HTML container element of where to
 *                                          create the search box
 */
function SearchBox (editor, container) {
    var searchBox = this;

    this.editor = editor;
    this.timeout = undefined;
    this.delay = 200; // ms
    this.lastText = undefined;

    this.dom = {};
    this.dom.container = container;

    var table = document.createElement('table');
    this.dom.table = table;
    table.className = 'search';
    container.appendChild(table);
    var tbody = document.createElement('tbody');
    this.dom.tbody = tbody;
    table.appendChild(tbody);
    var tr = document.createElement('tr');
    tbody.appendChild(tr);

    var td = document.createElement('td');
    tr.appendChild(td);
    var results = document.createElement('div');
    this.dom.results = results;
    results.className = 'results';
    td.appendChild(results);

    td = document.createElement('td');
    tr.appendChild(td);
    var divInput = document.createElement('div');
    this.dom.input = divInput;
    divInput.className = 'frame';
    divInput.title = 'Search fields and values';
    td.appendChild(divInput);

    // table to contain the text input and search button
    var tableInput = document.createElement('table');
    divInput.appendChild(tableInput);
    var tbodySearch = document.createElement('tbody');
    tableInput.appendChild(tbodySearch);
    tr = document.createElement('tr');
    tbodySearch.appendChild(tr);

    var refreshSearch = document.createElement('button');
    refreshSearch.className = 'refresh';
    td = document.createElement('td');
    td.appendChild(refreshSearch);
    tr.appendChild(td);

    var search = document.createElement('input');
    this.dom.search = search;
    search.oninput = function (event) {
        searchBox._onDelayedSearch(event);
    };
    search.onchange = function (event) { // For IE 8
        searchBox._onSearch(event);
    };
    search.onkeydown = function (event) {
        searchBox._onKeyDown(event);
    };
    search.onkeyup = function (event) {
        searchBox._onKeyUp(event);
    };
    refreshSearch.onclick = function (event) {
        search.select();
    };

    // TODO: ESC in FF restores the last input, is a FF bug, https://bugzilla.mozilla.org/show_bug.cgi?id=598819
    td = document.createElement('td');
    td.appendChild(search);
    tr.appendChild(td);

    var searchNext = document.createElement('button');
    searchNext.title = 'Next result (Enter)';
    searchNext.className = 'next';
    searchNext.onclick = function () {
        searchBox.next();
    };
    td = document.createElement('td');
    td.appendChild(searchNext);
    tr.appendChild(td);

    var searchPrevious = document.createElement('button');
    searchPrevious.title = 'Previous result (Shift+Enter)';
    searchPrevious.className = 'previous';
    searchPrevious.onclick = function () {
        searchBox.previous();
    };
    td = document.createElement('td');
    td.appendChild(searchPrevious);
    tr.appendChild(td);
}

/**
 * Go to the next search result
 * @param {boolean} [focus]   If true, focus will be set to the next result
 *                            focus is false by default.
 */
SearchBox.prototype.next = function(focus) {
    if (this.results != undefined) {
        var index = (this.resultIndex != undefined) ? this.resultIndex + 1 : 0;
        if (index > this.results.length - 1) {
            index = 0;
        }
        this._setActiveResult(index, focus);
    }
};

/**
 * Go to the prevous search result
 * @param {boolean} [focus]   If true, focus will be set to the next result
 *                            focus is false by default.
 */
SearchBox.prototype.previous = function(focus) {
    if (this.results != undefined) {
        var max = this.results.length - 1;
        var index = (this.resultIndex != undefined) ? this.resultIndex - 1 : max;
        if (index < 0) {
            index = max;
        }
        this._setActiveResult(index, focus);
    }
};

/**
 * Set new value for the current active result
 * @param {Number} index
 * @param {boolean} [focus]   If true, focus will be set to the next result.
 *                            focus is false by default.
 * @private
 */
SearchBox.prototype._setActiveResult = function(index, focus) {
    // de-activate current active result
    if (this.activeResult) {
        var prevNode = this.activeResult.node;
        var prevElem = this.activeResult.elem;
        if (prevElem == 'field') {
            delete prevNode.searchFieldActive;
        }
        else {
            delete prevNode.searchValueActive;
        }
        prevNode.updateDom();
    }

    if (!this.results || !this.results[index]) {
        // out of range, set to undefined
        this.resultIndex = undefined;
        this.activeResult = undefined;
        return;
    }

    this.resultIndex = index;

    // set new node active
    var node = this.results[this.resultIndex].node;
    var elem = this.results[this.resultIndex].elem;
    if (elem == 'field') {
        node.searchFieldActive = true;
    }
    else {
        node.searchValueActive = true;
    }
    this.activeResult = this.results[this.resultIndex];
    node.updateDom();

    // TODO: not so nice that the focus is only set after the animation is finished
    node.scrollTo(function () {
        if (focus) {
            node.focus(elem);
        }
    });
};

/**
 * Cancel any running onDelayedSearch.
 * @private
 */
SearchBox.prototype._clearDelay = function() {
    if (this.timeout != undefined) {
        clearTimeout(this.timeout);
        delete this.timeout;
    }
};

/**
 * Start a timer to execute a search after a short delay.
 * Used for reducing the number of searches while typing.
 * @param {Event} event
 * @private
 */
SearchBox.prototype._onDelayedSearch = function (event) {
    // execute the search after a short delay (reduces the number of
    // search actions while typing in the search text box)
    this._clearDelay();
    var searchBox = this;
    this.timeout = setTimeout(function (event) {
            searchBox._onSearch(event);
        },
        this.delay);
};

/**
 * Handle onSearch event
 * @param {Event} event
 * @param {boolean} [forceSearch]  If true, search will be executed again even
 *                                 when the search text is not changed.
 *                                 Default is false.
 * @private
 */
SearchBox.prototype._onSearch = function (event, forceSearch) {
    this._clearDelay();

    var value = this.dom.search.value;
    var text = (value.length > 0) ? value : undefined;
    if (text != this.lastText || forceSearch) {
        // only search again when changed
        this.lastText = text;
        this.results = this.editor.search(text);
        this._setActiveResult(undefined);

        // display search results
        if (text != undefined) {
            var resultCount = this.results.length;
            switch (resultCount) {
                case 0: this.dom.results.innerHTML = 'no&nbsp;results'; break;
                case 1: this.dom.results.innerHTML = '1&nbsp;result'; break;
                default: this.dom.results.innerHTML = resultCount + '&nbsp;results'; break;
            }
        }
        else {
            this.dom.results.innerHTML = '';
        }
    }
};

/**
 * Handle onKeyDown event in the input box
 * @param {Event} event
 * @private
 */
SearchBox.prototype._onKeyDown = function (event) {
    event = event || window.event;
    var keynum = event.which || event.keyCode;
    if (keynum == 27) { // ESC
        this.dom.search.value = '';  // clear search
        this._onSearch(event);
        util.preventDefault(event);
        util.stopPropagation(event);
    }
    else if (keynum == 13) { // Enter
        if (event.ctrlKey) {
            // force to search again
            this._onSearch(event, true);
        }
        else if (event.shiftKey) {
            // move to the previous search result
            this.previous();
        }
        else {
            // move to the next search result
            this.next();
        }
        util.preventDefault(event);
        util.stopPropagation(event);
    }
};

/**
 * Handle onKeyUp event in the input box
 * @param {Event} event
 * @private
 */
SearchBox.prototype._onKeyUp = function (event) {
    event = event || window.event;
    var keynum = event.which || event.keyCode;
    if (keynum != 27 && keynum != 13) { // !show and !Enter
        this._onDelayedSearch(event);   // For IE 8
    }
};

/**
 * The highlighter can highlight/unhighlight a node, and
 * animate the visibility of a context menu.
 * @constructor Highlighter
 */
function Highlighter () {
    this.locked = false;
}

/**
 * Hightlight given node and its childs
 * @param {Node} node
 */
Highlighter.prototype.highlight = function (node) {
    if (this.locked) {
        return;
    }

    if (this.node != node) {
        // unhighlight current node
        if (this.node) {
            this.node.setHighlight(false);
        }

        // highlight new node
        this.node = node;
        this.node.setHighlight(true);
    }

    // cancel any current timeout
    this._cancelUnhighlight();
};

/**
 * Unhighlight currently highlighted node.
 * Will be done after a delay
 */
Highlighter.prototype.unhighlight = function () {
    if (this.locked) {
        return;
    }

    var me = this;
    if (this.node) {
        this._cancelUnhighlight();

        // do the unhighlighting after a small delay, to prevent re-highlighting
        // the same node when moving from the drag-icon to the contextmenu-icon
        // or vice versa.
        this.unhighlightTimer = setTimeout(function () {
            me.node.setHighlight(false);
            me.node = undefined;
            me.unhighlightTimer = undefined;
        }, 0);
    }
};

/**
 * Cancel an unhighlight action (if before the timeout of the unhighlight action)
 * @private
 */
Highlighter.prototype._cancelUnhighlight = function () {
    if (this.unhighlightTimer) {
        clearTimeout(this.unhighlightTimer);
        this.unhighlightTimer = undefined;
    }
};

/**
 * Lock highlighting or unhighlighting nodes.
 * methods highlight and unhighlight do not work while locked.
 */
Highlighter.prototype.lock = function () {
    this.locked = true;
};

/**
 * Unlock highlighting or unhighlighting nodes
 */
Highlighter.prototype.unlock = function () {
    this.locked = false;
};

// create namespace
util = {};

// http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
if(!Array.prototype.indexOf) {
    Array.prototype.indexOf = function(obj){
        for(var i = 0; i < this.length; i++){
            if(this[i] == obj){
                return i;
            }
        }
        return -1;
    }
}

// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
if (!Array.prototype.forEach) {
    Array.prototype.forEach = function(fn, scope) {
        for(var i = 0, len = this.length; i < len; ++i) {
            fn.call(scope || this, this[i], i, this);
        }
    }
}

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
if(!Array.isArray) {
    Array.isArray = function (vArg) {
        return Object.prototype.toString.call(vArg) === "[object Array]";
    };
}

/**
 * Parse JSON using the parser built-in in the browser.
 * On exception, the jsonString is validated and a detailed error is thrown.
 * @param {String} jsonString
 */
util.parse = function parse(jsonString) {
    try {
        return JSON.parse(jsonString);
    }
    catch (err) {
        // try to throw a more detailed error message using validate
        util.validate(jsonString);
        throw err;
    }
};

/**
 * Validate a string containing a JSON object
 * This method uses JSONLint to validate the String. If JSONLint is not
 * available, the built-in JSON parser of the browser is used.
 * @param {String} jsonString   String with an (invalid) JSON object
 * @throws Error
 */
util.validate = function validate(jsonString) {
    if (typeof(jsonlint) != 'undefined') {
        jsonlint.parse(jsonString);
    }
    else {
        JSON.parse(jsonString);
    }
};

/**
 * Extend object a with the properties of object b
 * @param {Object} a
 * @param {Object} b
 * @return {Object} a
 */
util.extend = function extend(a, b) {
    for (var prop in b) {
        if (b.hasOwnProperty(prop)) {
            a[prop] = b[prop];
        }
    }
    return a;
};

/**
 * Remove all properties from object a
 * @param {Object} a
 * @return {Object} a
 */
util.clear = function clear (a) {
    for (var prop in a) {
        if (a.hasOwnProperty(prop)) {
            delete a[prop];
        }
    }
    return a;
};

/**
 * Output text to the console, if console is available
 * @param {...*} args
 */
util.log = function log (args) {
    if (console && typeof console.log === 'function') {
        console.log.apply(console, arguments);
    }
};

/**
 * Get the type of an object
 * @param {*} object
 * @return {String} type
 */
util.type = function type (object) {
    if (object === null) {
        return 'null';
    }
    if (object === undefined) {
        return 'undefined';
    }
    if ((object instanceof Number) || (typeof object === 'number')) {
        return 'number';
    }
    if ((object instanceof String) || (typeof object === 'string')) {
        return 'string';
    }
    if ((object instanceof Boolean) || (typeof object === 'boolean')) {
        return 'boolean';
    }
    if ((object instanceof RegExp) || (typeof object === 'regexp')) {
        return 'regexp';
    }
    if (Array.isArray(object)) {
        return 'array';
    }

    return 'object';
};

/**
 * Test whether a text contains a url (matches when a string starts
 * with 'http://*' or 'https://*' and has no whitespace characters)
 * @param {String} text
 */
var isUrlRegex = /^https?:\/\/\S+$/;
util.isUrl = function isUrl (text) {
    return (typeof text == 'string' || text instanceof String) &&
        isUrlRegex.test(text);
};

/**
 * Retrieve the absolute left value of a DOM element
 * @param {Element} elem    A dom element, for example a div
 * @return {Number} left    The absolute left position of this element
 *                          in the browser page.
 */
util.getAbsoluteLeft = function getAbsoluteLeft(elem) {
    var left = elem.offsetLeft;
    var body = document.body;
    var e = elem.offsetParent;
    while (e != null && elem != body) {
        left += e.offsetLeft;
        left -= e.scrollLeft;
        e = e.offsetParent;
    }
    return left;
};

/**
 * Retrieve the absolute top value of a DOM element
 * @param {Element} elem    A dom element, for example a div
 * @return {Number} top    The absolute top position of this element
 *                          in the browser page.
 */
util.getAbsoluteTop = function getAbsoluteTop(elem) {
    var top = elem.offsetTop;
    var body = document.body;
    var e = elem.offsetParent;
    while (e != null && e != body) {
        top += e.offsetTop;
        top -= e.scrollTop;
        e = e.offsetParent;
    }
    return top;
};

/**
 * Get the absolute, vertical mouse position from an event.
 * @param {Event} event
 * @return {Number} mouseY
 */
util.getMouseY = function getMouseY(event) {
    var mouseY;
    if ('pageY' in event) {
        mouseY = event.pageY;
    }
    else {
        // for IE8 and older
        mouseY = (event.clientY + document.documentElement.scrollTop);
    }

    return mouseY;
};

/**
 * Get the absolute, horizontal mouse position from an event.
 * @param {Event} event
 * @return {Number} mouseX
 */
util.getMouseX = function getMouseX(event) {
    var mouseX;
    if ('pageX' in event) {
        mouseX = event.pageX;
    }
    else {
        // for IE8 and older
        mouseX = (event.clientX + document.documentElement.scrollLeft);
    }

    return mouseX;
};

/**
 * Get the window height
 * @return {Number} windowHeight
 */
util.getWindowHeight = function getWindowHeight() {
    if ('innerHeight' in window) {
        return window.innerHeight;
    }
    else {
        // for IE8 and older
        return Math.max(document.body.clientHeight,
            document.documentElement.clientHeight);
    }
};

/**
 * add a className to the given elements style
 * @param {Element} elem
 * @param {String} className
 */
util.addClassName = function addClassName(elem, className) {
    var classes = elem.className.split(' ');
    if (classes.indexOf(className) == -1) {
        classes.push(className); // add the class to the array
        elem.className = classes.join(' ');
    }
};

/**
 * add a className to the given elements style
 * @param {Element} elem
 * @param {String} className
 */
util.removeClassName = function removeClassName(elem, className) {
    var classes = elem.className.split(' ');
    var index = classes.indexOf(className);
    if (index != -1) {
        classes.splice(index, 1); // remove the class from the array
        elem.className = classes.join(' ');
    }
};

/**
 * Strip the formatting from the contents of a div
 * the formatting from the div itself is not stripped, only from its childs.
 * @param {Element} divElement
 */
util.stripFormatting = function stripFormatting(divElement) {
    var childs = divElement.childNodes;
    for (var i = 0, iMax = childs.length; i < iMax; i++) {
        var child = childs[i];

        // remove the style
        if (child.style) {
            // TODO: test if child.attributes does contain style
            child.removeAttribute('style');
        }

        // remove all attributes
        var attributes = child.attributes;
        if (attributes) {
            for (var j = attributes.length - 1; j >= 0; j--) {
                var attribute = attributes[j];
                if (attribute.specified == true) {
                    child.removeAttribute(attribute.name);
                }
            }
        }

        // recursively strip childs
        util.stripFormatting(child);
    }
};

/**
 * Set focus to the end of an editable div
 * code from Nico Burns
 * http://stackoverflow.com/users/140293/nico-burns
 * http://stackoverflow.com/questions/1125292/how-to-move-cursor-to-end-of-contenteditable-entity
 * @param {Element} contentEditableElement   A content editable div
 */
util.setEndOfContentEditable = function setEndOfContentEditable(contentEditableElement) {
    var range, selection;
    if(document.createRange) {//Firefox, Chrome, Opera, Safari, IE 9+
        range = document.createRange();//Create a range (a range is a like the selection but invisible)
        range.selectNodeContents(contentEditableElement);//Select the entire contents of the element with the range
        range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
        selection = window.getSelection();//get the selection object (allows you to change selection)
        selection.removeAllRanges();//remove any selections already made
        selection.addRange(range);//make the range you have just created the visible selection
    }
    else if(document.selection) {//IE 8 and lower
        range = document.body.createTextRange();//Create a range (a range is a like the selection but invisible)
        range.moveToElementText(contentEditableElement);//Select the entire contents of the element with the range
        range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
        range.select();//Select the range (make it the visible selection
    }
};

/**
 * Select all text of a content editable div.
 * http://stackoverflow.com/a/3806004/1262753
 * @param {Element} contentEditableElement   A content editable div
 */
util.selectContentEditable = function selectContentEditable(contentEditableElement) {
    if (!contentEditableElement || contentEditableElement.nodeName != 'DIV') {
        return;
    }

    var sel, range;
    if (window.getSelection && document.createRange) {
        range = document.createRange();
        range.selectNodeContents(contentEditableElement);
        sel = window.getSelection();
        sel.removeAllRanges();
        sel.addRange(range);
    } else if (document.body.createTextRange) {
        range = document.body.createTextRange();
        range.moveToElementText(contentEditableElement);
        range.select();
    }
};

/**
 * Get text selection
 * http://stackoverflow.com/questions/4687808/contenteditable-selected-text-save-and-restore
 * @return {Range | TextRange | null} range
 */
util.getSelection = function getSelection() {
    if (window.getSelection) {
        var sel = window.getSelection();
        if (sel.getRangeAt && sel.rangeCount) {
            return sel.getRangeAt(0);
        }
    } else if (document.selection && document.selection.createRange) {
        return document.selection.createRange();
    }
    return null;
};

/**
 * Set text selection
 * http://stackoverflow.com/questions/4687808/contenteditable-selected-text-save-and-restore
 * @param {Range | TextRange | null} range
 */
util.setSelection = function setSelection(range) {
    if (range) {
        if (window.getSelection) {
            var sel = window.getSelection();
            sel.removeAllRanges();
            sel.addRange(range);
        } else if (document.selection && range.select) {
            range.select();
        }
    }
};

/**
 * Get selected text range
 * @return {Object} params  object containing parameters:
 *                              {Number}  startOffset
 *                              {Number}  endOffset
 *                              {Element} container  HTML element holding the
 *                                                   selected text element
 *                          Returns null if no text selection is found
 */
util.getSelectionOffset = function getSelectionOffset() {
    var range = util.getSelection();

    if (range && 'startOffset' in range && 'endOffset' in range &&
            range.startContainer && (range.startContainer == range.endContainer)) {
        return {
            startOffset: range.startOffset,
            endOffset: range.endOffset,
            container: range.startContainer.parentNode
        };
    }
    else {
        // TODO: implement getSelectionOffset for IE8
    }

    return null;
};

/**
 * Set selected text range in given element
 * @param {Object} params   An object containing:
 *                              {Element} container
 *                              {Number} startOffset
 *                              {Number} endOffset
 */
util.setSelectionOffset = function setSelectionOffset(params) {
    if (document.createRange && window.getSelection) {
        var selection = window.getSelection();
        if(selection) {
            var range = document.createRange();
            // TODO: do not suppose that the first child of the container is a textnode,
            //       but recursively find the textnodes
            range.setStart(params.container.firstChild, params.startOffset);
            range.setEnd(params.container.firstChild, params.endOffset);

            util.setSelection(range);
        }
    }
    else {
        // TODO: implement setSelectionOffset for IE8
    }
};

/**
 * Get the inner text of an HTML element (for example a div element)
 * @param {Element} element
 * @param {Object} [buffer]
 * @return {String} innerText
 */
util.getInnerText = function getInnerText(element, buffer) {
    var first = (buffer == undefined);
    if (first) {
        buffer = {
            'text': '',
            'flush': function () {
                var text = this.text;
                this.text = '';
                return text;
            },
            'set': function (text) {
                this.text = text;
            }
        };
    }

    // text node
    if (element.nodeValue) {
        return buffer.flush() + element.nodeValue;
    }

    // divs or other HTML elements
    if (element.hasChildNodes()) {
        var childNodes = element.childNodes;
        var innerText = '';

        for (var i = 0, iMax = childNodes.length; i < iMax; i++) {
            var child = childNodes[i];

            if (child.nodeName == 'DIV' || child.nodeName == 'P') {
                var prevChild = childNodes[i - 1];
                var prevName = prevChild ? prevChild.nodeName : undefined;
                if (prevName && prevName != 'DIV' && prevName != 'P' && prevName != 'BR') {
                    innerText += '\n';
                    buffer.flush();
                }
                innerText += util.getInnerText(child, buffer);
                buffer.set('\n');
            }
            else if (child.nodeName == 'BR') {
                innerText += buffer.flush();
                buffer.set('\n');
            }
            else {
                innerText += util.getInnerText(child, buffer);
            }
        }

        return innerText;
    }
    else {
        if (element.nodeName == 'P' && util.getInternetExplorerVersion() != -1) {
            // On Internet Explorer, a <p> with hasChildNodes()==false is
            // rendered with a new line. Note that a <p> with
            // hasChildNodes()==true is rendered without a new line
            // Other browsers always ensure there is a <br> inside the <p>,
            // and if not, the <p> does not render a new line
            return buffer.flush();
        }
    }

    // br or unknown
    return '';
};

/**
 * Returns the version of Internet Explorer or a -1
 * (indicating the use of another browser).
 * Source: http://msdn.microsoft.com/en-us/library/ms537509(v=vs.85).aspx
 * @return {Number} Internet Explorer version, or -1 in case of an other browser
 */
util.getInternetExplorerVersion = function getInternetExplorerVersion() {
    if (_ieVersion == -1) {
        var rv = -1; // Return value assumes failure.
        if (navigator.appName == 'Microsoft Internet Explorer')
        {
            var ua = navigator.userAgent;
            var re  = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
            if (re.exec(ua) != null) {
                rv = parseFloat( RegExp.$1 );
            }
        }

        _ieVersion = rv;
    }

    return _ieVersion;
};

/**
 * cached internet explorer version
 * @type {Number}
 * @private
 */
var _ieVersion = -1;

/**
 * Add and event listener. Works for all browsers
 * @param {Element}     element    An html element
 * @param {string}      action     The action, for example "click",
 *                                 without the prefix "on"
 * @param {function}    listener   The callback function to be executed
 * @param {boolean}     [useCapture] false by default
 * @return {function}   the created event listener
 */
util.addEventListener = function addEventListener(element, action, listener, useCapture) {
    if (element.addEventListener) {
        if (useCapture === undefined)
            useCapture = false;

        if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
            action = "DOMMouseScroll";  // For Firefox
        }

        element.addEventListener(action, listener, useCapture);
        return listener;
    } else {
        // IE browsers
        var f = function () {
            return listener.call(element, window.event);
        };
        element.attachEvent("on" + action, f);
        return f;
    }
};

/**
 * Remove an event listener from an element
 * @param {Element}  element   An html dom element
 * @param {string}   action    The name of the event, for example "mousedown"
 * @param {function} listener  The listener function
 * @param {boolean}  [useCapture]   false by default
 */
util.removeEventListener = function removeEventListener(element, action, listener, useCapture) {
    if (element.removeEventListener) {
        // non-IE browsers
        if (useCapture === undefined)
            useCapture = false;

        if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
            action = "DOMMouseScroll";  // For Firefox
        }

        element.removeEventListener(action, listener, useCapture);
    } else {
        // IE browsers
        element.detachEvent("on" + action, listener);
    }
};


/**
 * Stop event propagation
 * @param {Event} event
 */
util.stopPropagation = function stopPropagation(event) {
    if (!event) {
        event = window.event;
    }

    if (event.stopPropagation) {
        event.stopPropagation();  // non-IE browsers
    }
    else {
        event.cancelBubble = true;  // IE browsers
    }
};


/**
 * Cancels the event if it is cancelable, without stopping further propagation of the event.
 * @param {Event} event
 */
util.preventDefault = function preventDefault(event) {
    if (!event) {
        event = window.event;
    }

    if (event.preventDefault) {
        event.preventDefault();  // non-IE browsers
    }
    else {
        event.returnValue = false;  // IE browsers
    }
};


// module exports
var jsoneditor = {
    'JSONEditor': JSONEditor,
    'JSONFormatter': function () {
        throw new Error('JSONFormatter is deprecated. ' +
            'Use JSONEditor with mode "text" or "code" instead');
    },
    'util': util
};

/**
 * load jsoneditor.css
 */
var loadCss = function () {
    // find the script named 'jsoneditor.js' or 'jsoneditor-min.js' or
    // 'jsoneditor.min.js', and use its path to find the css file to be
    // loaded.
    var scripts = document.getElementsByTagName('script');
    for (var s = 0; s < scripts.length; s++) {
        var src = scripts[s].src;
        if (/(^|\/)jsoneditor([-\.]min)?.js$/.test(src)) {
            var jsFile = src.split('?')[0];
            var cssFile = jsFile.substring(0, jsFile.length - 2) + 'css';

            // load css file
            var link = document.createElement('link');
            link.type = 'text/css';
            link.rel = 'stylesheet';
            link.href = cssFile;
            document.getElementsByTagName('head')[0].appendChild(link);

            break;
        }
    }
};

/**
 * CommonJS module exports
 */
if (typeof(module) != 'undefined' && typeof(exports) != 'undefined') {
    loadCss();
    module.exports = exports = jsoneditor;
}

/**
 * AMD module exports
 */
if (typeof(require) != 'undefined' && typeof(define) != 'undefined') {
    loadCss();
    define(function () {
        return jsoneditor;
    });
}
else {
    // attach the module to the window, load as a regular javascript file
    window['jsoneditor'] = jsoneditor;
}


})();
